《C++性能优化指南》第四章:优化字符串的使用

第四章:优化字符串的使用

针对C++的std::string进行讲解。

4.1 字符串的三个特性

(1)字符串是动态分配的
  原因:字符串内部的字符缓冲区的大小是固定的,当有使字符串变长的操作时,可能会使字符串的长度超出它内部的缓冲区的大小,从而发生从内存管理器中 malloc/new 一块新的缓冲区,并将字符串 copy 到新的缓冲区中,并 free/delete 原来的空间。
  解释上文中出现可能性的原因,是有些字符串的实现方式所申请的字符缓冲区的大小是需要存储字符数的 2 倍。

(2)字符串是值,而非引用
  也就是说,要把字符串当成一个整体对待,不能看成是组合的字节。比如:
  1)赋值语句 = copy:一个字符串赋值给另一个字符串时,每个字符串变量都拥有一份它们所保存的内容的私有副本。
  2)表达式 = 存在中间临时值:字符串表达式的中间结果也是值。
    比如:s1 = s2 + s3 + s4;
     s2 + s3 会malloc新的临时字符串并copy,+ s4 会再malloc新的临时字符串并copy和free,= 会取代 s1 之前的值并free,所以总共有2次malloc,2次free,5次copy。
    

(3)字符串会进行大量复制
  因为字符串是以值的方式来处理的,当创建字符串、赋值、或将其作为参数传递给函数时,都会进行一次copy,并且赋值和参数传递的开销都会变的很大,但变值函数mutating function和非常量引用的开销却很小。
  如果以写时复制 copy on write,COW 方式实现字符串,那么赋值和参数传递操作的开销就很小,但是一旦字符串被共享了,非常量引用以及任何变值函数的调用,都需要昂贵的分配和复制操作。在并发代码中,写时复制字符串的开销同样很大。每次变值函数和非常量引用都要访问引用计数器。当引用计数器被多个线程访问时,每个线程都必须使用一个特殊的指令从主内存中得到引用计数的副本,以确保没有其他线程改变这个值。所以,实际上,写时复制甚至是不符合C++11标准的实现方式,而且问题百出。
  “右值引用” 和 “移动语义”,调用移动构造函数,而非复制构造函数。

4.2 案例:优化字符串的内存分配和复制操作

# 待优化的函数
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;
}

优化方法:

(1) 使用复合赋值操作避免临时字符串

result += s[i]; perf boost +13x

  因为移除了所有 1)为了new临时字符串对象来保存连接结果而对内存管理器的调用,2)相关的copy, 3)delete临时字符串的操作。

(2) 使用预留存储空间减少内存的重新分配

result.reserve(s.length()); perf boost +17%

  因为reserve()函数会预先分配足够的内存空间,不仅移除了字符串缓冲区的重新分配,还改善了函数所读取数据的缓存局部性cache locality。

(3)消除参数字符串的复制,但需要同时使用迭代器消除指针解引

# 常量引用 作为函数参数
remove_ctrl(std::string const& s) perf boost -8%

  变量的引用是通过指针实现的,当程序中每次出现s时,都会发生解引指针的操作,会导致perf下降,所以迭代器可以节省解引操作。

for (auto it=s.begin(),end=s.end(); it != end; ++it) {  perf boost +41%
    if (*it >= 0x20)
        result += *it;
}

  以上实现了迭代器操作,并且在循环初始化时缓存 s.end(), 节省调用开销。
  函数参数改用 字符串引用 remove_ctrl(std::string& s) ,也能提升 perf +16%,但是没有常量引用提升的更多。

(4)消除对返回的字符串的复制

remove_ctrl(std::string& result, std::string const& s) {  perf boost +2%
    result.clear();
    ...
}

  有少许的性能提升,但需要修改接口。

(5)用字符数组替代字符串

void remove_ctr_cstring(char* destp, char const* scrp, size_t size) {  perf boost +6x
    for (int size_t i=0; i < size; ++i) {
        if (srcp[i] >= 0x20)
             *desp++ = scrp[i];
    }
    *desp = 0;
}

  使用C的静态数组替换C++的string字符串,需静态地声明大型临时缓冲区。性能提升的原因是移除了若干函数的调用以及改善了缓存局部性,但接口改变巨大。

总结:优化的深度由性能提高的指标来决定,性能提高的越多,工作量越大,采用2/8原则。

4.2 案例:采用其他方法优化字符串

(1)使用更好的算法

for (size_t b=0, i=b, e=s.length(); b < e; b=i+1) {
    for (i=b; i < e; ++i) {
        if (s[i] < 0x20)
            break;
    }
    result = result + s.substr(b, i-b);
}
b   i+1     e
|   |       |
xxxxxxx...xxx
|__|_|
i

  以上算法实现的是,一个子字符串的copy,而非单个字符的copy。
或者

result.append(s, b, i-b); 添加子串,而非创建临时字串。
s.erase(i, 1); 删除某字符,而非创建临时字串。

(2)使用更好的编译器
  icc

(3)使用更好的字符串库
  boost 字符串库
  C++字符串工具包StrTk
  std::stringstream,该方法封装了一块动态大小的缓冲区,不会创建任何临时字符串,因此不会发生内存分配和复制操作,优点是可以避免写出低效的字符串代码,但性能永远不会胜过std::string。
  std::string_view

(4)使用更好的内存分配器
  tcmalloc

注意点:
(1)"MyString" 是字符串常量,与 std::string 是不同的,在分配内存和复制操作中会发生转换操作。
(2)不同字符集之间的转换,相对ASCII、UTF-16等,UTF-8是更通用的字符集。

猜你喜欢

转载自www.cnblogs.com/qccz123456/p/11917996.html