C++性能优化-字符串的优化
字符串的優(yōu)化
糟糕的字符串連接函數(shù)
在C++中字符串是按照值的形式實(shí)現(xiàn)的,又因?yàn)镃++中字符串底層是使用動態(tài)內(nèi)存實(shí)現(xiàn)的,因此、在項(xiàng)目中對字符串的優(yōu)化必不可少,也是性能優(yōu)化的重點(diǎn)。
假如代碼中有如下remove_ctrl函數(shù)的實(shí)現(xiàn):
std::string remove_ctrl(std::string s) {std::string result;for (int i=0; i<s.length(); ++i) {if(s[i] >= 0x20)result = result + s[i];}return result; }remove_ctrl() 在循環(huán)中對通過參數(shù)接收到的字符串 s 的每個(gè)字符進(jìn)行處理。循環(huán)中的代碼就是導(dǎo)致這個(gè)函數(shù)成為熱點(diǎn)的原因。 if 條件語句從字符串中得到一個(gè)字符,然后與一個(gè)字面常量進(jìn)行比較。這里沒有什么問題。但是第 5 行的語句就不一樣了。
正如之前所指出的,字符串連接運(yùn)算符的開銷是很大的。它會調(diào)用內(nèi)存管理器去構(gòu)建一個(gè)新的臨時(shí)字符串對象來保存連接后的字符串。如果傳遞給 remove_ctrl() 的參數(shù)是一個(gè)由大于0x2(可打印)的字符組成的字符串,那么 remove_ctrl() 幾乎會為 s 中的每個(gè)字符都構(gòu)建一個(gè)臨時(shí)字符串對象。對于一個(gè)由 100 個(gè)字符組成的字符串而言,這會調(diào)用 100 次內(nèi)存管理器來為臨時(shí)字符串分配內(nèi)存,調(diào)用 100 次內(nèi)存管理器來釋放內(nèi)存
對上述代碼進(jìn)行基準(zhǔn)測試,結(jié)果如下:
------------------------------------------------------------------------- Benchmark Time CPU Iterations ------------------------------------------------------------------------- bench_remove_ctrl_operator 12433 ns 12432 ns 54195如果你的代碼編輯器帶自動檢查,就會發(fā)現(xiàn)這樣的代碼連代碼編輯器都受不了,給你顯示一大堆波浪號。
使用復(fù)合賦值操作避免臨時(shí)字符串
注意觀察上面實(shí)現(xiàn)的第5行代碼result = result + s[i];,因?yàn)樽址前凑罩堤幚淼?#xff0c;等號右邊的值想加后會在挨個(gè)賦值到等號左邊,這樣如果參數(shù)字符串有n個(gè)字符,那么remove_ctrl會復(fù)制O(n[^2])個(gè)自負(fù),所有這些內(nèi)存分配和復(fù)制都會導(dǎo)致性能變差。
為了減少字符串的復(fù)制,可以使用復(fù)合操作符+=,使用復(fù)合操作符之后的代碼如下:
std::string remove_ctrl(std::string s) {std::string result;for (int i=0; i<s.length(); ++i) {if(s[i] >= 0x20)result += s[i];}return result; }改進(jìn)之后測試結(jié)果:
------------------------------------------------------------------------- Benchmark Time CPU Iterations ------------------------------------------------------------------------- bench_remove_ctrl_operator 12433 ns 12432 ns 54195 bench_remove_ctrl_operator_opt 12400 ns 12400 ns 56281可以看出測試結(jié)果中改進(jìn)之后的代碼明顯比沒有改進(jìn)之前快很多,而且這個(gè)時(shí)間會隨著傳入的字符串的長度增長而增加,傳入的字符串越長,沒有改進(jìn)的代碼需要復(fù)制的就越多。
通過預(yù)留存儲空間來減少內(nèi)存的重新分配
經(jīng)過上述優(yōu)化之后,result仍然存在在執(zhí)行的過程中,由于緩存區(qū)不夠需要重新分配的情況,導(dǎo)致頻繁分配內(nèi)存,可以通過提前分配內(nèi)存優(yōu)化該問題,結(jié)果如下:
std::string remove_ctrl_reserve(std::string s) {std::string result;result.reserve(s.length());for (int i=0; i< (int)s.length(); ++i) {if(s[i] >= 0x20)result += s[i];}return result; }運(yùn)行結(jié)果:
------------------------------------------------------------------------- Benchmark Time CPU Iterations ------------------------------------------------------------------------- bench_remove_ctrl_operator 12170 ns 12168 ns 56990 bench_remove_ctrl_operator_opt 12390 ns 12388 ns 57518 bench_remove_ctrl_reserve 767 ns 767 ns 908214消除對參數(shù)字符串的復(fù)制
上述方法中還是會對參數(shù)字符串進(jìn)行一次復(fù)制,由于內(nèi)存分配是非常昂貴的,因此哪怕是一次內(nèi)存分配也值得從程序中去除,優(yōu)化之后參數(shù)按照引用傳遞,結(jié)果如下:
std::string remove_ctrl_ref_args(std::string &s) {std::string result;result.reserve(s.length());for (int i=0; i< (int)s.length(); ++i) {if(s[i] >= 0x20)result += s[i];}return result; }更改之后運(yùn)行結(jié)果:
------------------------------------------------------------------------- Benchmark Time CPU Iterations ------------------------------------------------------------------------- bench_remove_ctrl_operator 12158 ns 12159 ns 56439 bench_remove_ctrl_operator_opt 12044 ns 12046 ns 57409 bench_remove_ctrl_reserve 762 ns 762 ns 921342 bench_remove_ctrl_ref_args 737 ns 738 ns 938882移除對返回值的復(fù)制
經(jīng)過上述的優(yōu)化之后,性能已經(jīng)有了很大的改善。但是仔細(xì)觀察代碼還是能發(fā)現(xiàn)存在不必要的內(nèi)存申請的地方,那就是返回值,每次在處理好字符串之后,返回時(shí)還需要復(fù)制一次,這時(shí)也會申請內(nèi)存。我們完全可以將字符串通過引用傳出去,改進(jìn)之后的代碼如下:
void remove_ctrl_ref_result_it(std::string &result, std::string const &s) {result.clear();result.reserve(s.length());for (auto &it:s) {if (it >= 0x20)result += it;} }改進(jìn)之后的運(yùn)行結(jié)果如下:
-------------------------------------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------------------------------- bench_remove_ctrl_operator 11948 ns 11949 ns 55770 bench_remove_ctrl_operator_opt 12088 ns 12089 ns 58264 bench_remove_ctrl_reserve 767 ns 767 ns 899659 bench_remove_ctrl_ref_args 742 ns 742 ns 948853 bench_remove_ctrl_ref_args_it 758 ns 758 ns 922450 bench_remove_ctrl_ref_result_it 493 ns 493 ns 1382138可以看到,當(dāng)返回值也通過引用返回時(shí),函數(shù)調(diào)用的耗時(shí)一下子降低到了493納秒。
用字符串代替數(shù)組
當(dāng)程序有嚴(yán)格的性能要求的時(shí)候,不是使用C++風(fēng)格的字符串,而應(yīng)該使用C風(fēng)格的字符數(shù)組,使用字符數(shù)組需要程序員自己控制內(nèi)存的申請和釋放,能夠最大程度提升性能,但是帶來的后果就是維護(hù)的成本增加
void remove_ctrl_cstrings(char* destp, char const* srcp, size_t size) {for (size_t i=0; i<size; ++i) {if (srcp[i] >= 0x20)*destp++ = srcp[i];}*destp = 0; }使用C風(fēng)格的字符數(shù)組測試結(jié)果:
-------------------------------------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------------------------------- bench_remove_ctrl_operator 12103 ns 12103 ns 55630 bench_remove_ctrl_operator_opt 12175 ns 12175 ns 57324 bench_remove_ctrl_reserve 765 ns 765 ns 918301 bench_remove_ctrl_ref_args 746 ns 746 ns 938322 bench_remove_ctrl_ref_args_it 751 ns 751 ns 912842 bench_remove_ctrl_ref_result_it 497 ns 497 ns 1394954 bench_remove_ctrl_cstrings 270 ns 270 ns 2575663使用更好的算法
有沒有即好維護(hù)又有高性能的的方法,答案是有的,那就是使用更好的算法。
經(jīng)過對以上的改進(jìn)的思考,我們發(fā)現(xiàn)只要想辦法能夠減少內(nèi)存的申請和釋放,字符串也能有很高的性能,這就是新算法需要下手的地方。
因?yàn)椤⒗系膶?shí)現(xiàn)方式,是每個(gè)值都要對比之后,挨個(gè)添加到result中,這就導(dǎo)致頻繁的操作內(nèi)存,新算法中使用append一段一段的取符合條件的字符串添加到result中,實(shí)際測試結(jié)果141納秒左右,已經(jīng)優(yōu)于C風(fēng)格的字符串,這就要感謝算法帶來的buff加成。
void remove_ctrl_block_append(std::string &result, const std::string &s) {result.clear();result.reserve(s.length());for (size_t b=0,i=b; b < s.length(); b = i+1) {for (i=b; i<s.length(); ++i) {if (s[i] < 0x20) break;}result.append(s, b, i-b);} }運(yùn)行結(jié)果:
-------------------------------------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------------------------------- bench_remove_ctrl_operator 12258 ns 12257 ns 54639 bench_remove_ctrl_operator_opt 12347 ns 12347 ns 56488 bench_remove_ctrl_reserve 765 ns 765 ns 896059 bench_remove_ctrl_ref_args 736 ns 736 ns 947049 bench_remove_ctrl_ref_args_it 746 ns 746 ns 926601 bench_remove_ctrl_ref_result_it 494 ns 494 ns 1394869 bench_remove_ctrl_cstrings 270 ns 270 ns 2562077 bench_remove_block_append 141 ns 141 ns 4941262當(dāng)然,還可以使用剔除不符合要求字符的方式實(shí)現(xiàn):
void remove_ctrl_erase(std::string &s) {for (size_t i = 0; i < s.length();)if (s[i] < 0x20)s.erase(i,1);else ++i; }測試結(jié)果:
-------------------------------------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------------------------------- bench_remove_ctrl_operator 12146 ns 12146 ns 56901 bench_remove_ctrl_operator_opt 12254 ns 12254 ns 56758 bench_remove_ctrl_reserve 756 ns 756 ns 909901 bench_remove_ctrl_ref_args 735 ns 735 ns 938884 bench_remove_ctrl_ref_args_it 748 ns 748 ns 922788 bench_remove_ctrl_ref_result_it 494 ns 494 ns 1422658 bench_remove_ctrl_cstrings 275 ns 275 ns 2534856 bench_remove_block_append 143 ns 143 ns 4871896 bench_remove_erase 169 ns 169 ns 4104307當(dāng)然、還可以使用更優(yōu)秀的庫,使用優(yōu)化性能更好的編譯器,或者為自己的業(yè)務(wù)專門定制一個(gè)字符串的類,來達(dá)到最優(yōu)。
總結(jié)
以上是生活随笔為你收集整理的C++性能优化-字符串的优化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: const函数和const对象
- 下一篇: C++优化热点语句