当前位置: 移动技术网 > IT编程>开发语言>C/C++ > C++性能优化笔记

C++性能优化笔记

2018年08月31日  | 移动技术网IT编程  | 我要评论

越狱第五季第九集,邓文仪,黄岩岛在哪里

  最近着手去优化项目中一个模块的性能。该模块是用c++实现,对大量文本数据进行处理脱敏。

   一开始时,没什么思路,因为不知道性能瓶颈在哪里。于是借助perf工具来对程序进行分析,找出程序的性能都消耗在哪里了。

下面对待优化的程序运行一遍,通过perf统计一下程序中哪些函数运行cpu周期占百分百最多。

我们直接看占用比靠前的这一部分,只需要把这些大头优化好,那么整体的性能就能得到提升。那些本来占用cpu周期很少的函数,再怎么优化都整体的性能也没有很大的改变。

 1 samples: 629k of event 'cpu-clock', event count (approx.): 157401000000
 2   children      self  command  shared object                 symbol
 3 +    8.87%     8.77%  mask     libc-2.12.so                  [.] __memcmp_sse4_1
 4 +    7.89%     7.79%  mask     libc-2.12.so                  [.] _int_malloc
 5 +    7.26%     7.18%  mask     libc-2.12.so                  [.] malloc
 6 +    6.80%     6.73%  mask     libc-2.12.so                  [.] _int_free
 7 +    4.82%     4.76%  mask     libstdc++.so.6.0.19           [.] std::string::_rep::_m_dispose
 8 +    4.06%     4.01%  mask     mask                          [.] std::_rb_tree<std::string, std::string, std::_identity<std::string>, std::less<std::string>, std::allocator<std::string> >::find 9 +    3.92%     3.87%  mask     libstdc++.so.6.0.19           [.] std::string::find
10 +    3.79%     3.74%  mask     mask                          [.] std::_rb_tree<std::string, std::pair<std::string const, std::string>, std::_select1st<std::pair<std::string const, std::string> >, std::less<std::string>, std::allocator<std::pair<std::string const, std::string> > >::find11 +    2.35%     2.30%  mask     libstdc++.so.6.0.19           [.] std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string
12 +    2.32%     2.30%  mask     libc-2.12.so                  [.] memcpy
13 +    2.27%     2.24%  mask     libstdc++.so.6.0.19           [.] std::string::assign
14 +    1.89%     1.87%  mask     libstdc++.so.6.0.19           [.] std::string::compare
15 +    1.56%     1.54%  mask     libstdc++.so.6.0.19           [.] std::string::append
16 +    1.43%     1.41%  mask     mask                          [.] std::map<std::string, long long, std::less<std::string>, std::allocator<std::pair<std::string const, long long> > >::operator[]17 +    1.37%     1.35%  mask     mask                          [.] metamasker::basemetaelementmasker::domask
18 +    1.24%     1.22%  mask     libstdc++.so.6.0.19           [.] std::string::_m_mutate
19 +    1.20%     1.19%  mask     libstdc++.so.6.0.19           [.] std::string::_rep::_m_dispose
20 +    1.19%     1.18%  mask     libc-2.12.so                  [.] free
21 +    1.17%     1.15%  mask     mask                          [.] cppjieba::trie::find
22 +    1.14%     1.13%  mask     mask                          [.] util::string::halffulltransformer::isremainfullchar
23 +    1.10%     1.09%  mask     libstdc++.so.6.0.19           [.] operator new
24 +    1.03%     1.03%  mask     [kernel.kallsyms]             [k] retint_careful
25 +    0.93%     0.92%  mask     libstdc++.so.6.0.19           [.] std::string::_rep::_s_create
26 +    0.92%     0.91%  mask     libstdc++.so.6.0.19           [.] std::string::_rep::_m_clone
27 +    0.92%     0.91%  mask     libc-2.12.so                  [.] malloc_consolidate
28 +    0.92%     0.91%  mask     libc-2.12.so                  [.] __strlen_sse42
29 +    0.85%     0.84%  mask     libstdc++.so.6.0.19           [.] std::string::reserve

  乍一看,基本都是c库和stl库的函数占用了大部分时间,自己实现的函数寥寥无几。

消耗时间最多的就是c库的内存分配和释放函数,再看看第11行,基本可以确认是因为代码中过多使用std::string对象,导致了内存频繁申请和释放。

代码中对字符串的处理,都是使用了string类来处理,我们做不到对string的内部的优化,也很难去实现一个比string很好的类,那么只能从string对象的使用上面入手。

 

1. string优化

1.1 参数的传递使用引用

  这里不止是string,当函数参数只是作为输入只读时,就应该使用常量引用传参。避免不必要的对象构造和释放,在传递大的对象时,效果相差很大。

1.2 变量延时定义

  string变量的定义,尽可能的放在必须要使用的时候再定义。有时候可能一个判断分支,导致一个预先定义的对象根本就没有使用,那就这个对象的构造和释放就是一个额外的消耗,这种情况必须避免。

1.3 string::find()

  就是在多次字符串查找时,应该合理记录上一次查找的位置,作为下一次查找的开始位置的依据。在查找同一个值时,不难理解,如果是在字符串中,每次查找不同的值时,可以根据实际情况去处理。比如从一个地址中,先查找“市”再查找“镇”,这种情况就不用每次都从开头开始查找。当字符串很大时,效率提升会比较明显。具体说明时候使用这种处理方法,就需要自己根据实际情况去考虑了。

1.4 string拼接与善用reserve()/resize()

  字符串拼接是一个很常用的操作,平时简单使用时,也不行要太多注意,怎么简单怎么来。但是,在处理大字符串时,效率就跟不上来了。

  例如,需要对数据库表的每一行数据拼接成一个字符串,行数据可能很大。在不断循环表行数进行拼接时,使用string重载的+操作就显得太慢了。string对象默认初始化的空间比较小,可能每次调用+操作时都需要重新分配空间。这样当拼接一个大字符串时,就需要分配释放多次空间,还需要进行内存拷贝,这是非常耗时的。

  我们应该在定义string对象时,直接指定分配空间的大小,这个值可以通过预估出来或者通过计算的来。

// 定义时
std::string str(1024, 0);

// reserve()
str.reserve(1024);

// size()
str.resize(1024);

  如果string对象时新定义的,可以直接调用构造函数来预分配空间大小。如果string对象已经定义了,可以使用reserve()来分配一个指定大小的空间。

  当预设好string对象的空间大小后,自己去用memcpy()和string::resize()去实现字符串拼接操作,这样效率上比用string的+操作要快。

1.5 string的copy-on-write机制

  当string赋值或者拷贝时都是浅拷贝,两个string对象的实际存储字符串的地址是同一个,string中用一个引用计数的变量,来记录当前有多少个string对象使用同一个字符串存储空间,类似于共享指针。当string对象需要修改时,这个时候才会重新分配一个空间,并把字符串拷贝到新空间,string对象指向新的空间,在新的地址空间中对字符串进行修改,这就是copy-on-write的意思。

  

2.map优化

  看到perf的分析报告,其中第8、10、16行,看到一些红黑树查找和map的operator[]使用的cpu周期占用的也挺多。由于脱敏中使用到脱敏映射表都是用std::map来实现,std::map内部是用红黑树来实现的,查找效率会比较慢。鉴于映射表中,不需要顺序保存,所以用查找效率更高的boost::unordered_map来代替std::map。

  map的内部实现是二叉平衡树(红黑树);unordered_map内部是一个hash_table。map是一个有序的容器,提供了稳定的插入删除查找效率,内存占用也相对较小;而unordered_map是无序容器,可以快速插入删除,查找效率也比map快,只是内存会占用得多一点。

2.1 unordered_map替换map

  在程序中,原来字典的映射表都是由map来实现的,这里把字典相关的映射表都改为用unordered_map代替。这样在脱敏的时候,通过映射表查找脱敏字符串时的效率有很大提升。

2.2 小心map::operator[]()

  map的operator[]()函数有个特性,当调用map[key]时,如果key在map中不存在,则会在map中插入一个键值对,键为key,值则默认构造一个值。map的operator[]()用起来是很方便,但是一但不注意,引入了map中不应该存在的值,很有可能就会带来一些不必要的麻烦。看下面的代码:

void children:set(std::map<std::string, std::string>& ret) {
    parent::set(ret);
    if(!ret["error"].empty()){
      return;
    }
}

int main() {
  children ch;
  std::map<std::string, std::string> ret;
  ch.set(ret);
  if(ret.find("error") != ret.end()){
    std::cout << "error" << std::endl;
  }
}

  子类调用了父类的set()函数后,判断结果是否有错误。这里使用了map::operator[](),就会有问题,在外部调用了子类的set()函数后,判断返回结果集中是否存在"error"的元素,最终结果输出"error"。

  所以在使用map的operator[]()函数时,一定要小心。除非你很明确知道这个key是在map中存在的,否则不要直接调用operator[]()。而应该先通过find()函数查找key是否存在于map中,再去获取key的值。

2.3 find()与operator[]()之间的使用

  map并不是一个顺序容器,map的[]操作与数组的[]操作不一样。map的operator[]()与find()的实现差不多,通过参数key查找到map中的键值对并返回不同的值,只不过operator[]()在map查找不到时会插入一个键值对。

  

int getvalue(int key){
    if(map.find(key) != map.end()){
        return map[key];        
    }
    return 0;    
}

  上面的代码,在调用operator[]()前,先用find()函数确保key存在map中,避免了2.2中提到的问题。但是在效率上却慢了,find()和operator[]()中都有查找算法,这里获取一个值就查找了两遍,这是不应该的。应改为一下这种写法:

int getvalue(int key){
    std::map<int,int>::iterator it = map.find(key);
    if(it != map.end()){
        return it->second;        
    }
    return 0;    
}

2.4 map::end()优化

 1 std::map<int, int> mapkv;
 2 int sum(std::vector<int> veckey){
 3     size_t size = veckey.size();
 4     int sum = 0;
 5     for(size_t i = 0; i < size; ++i){
 6         std::map<int, int>::iterator it = mapkv.find(veckey[i]);
 7         if(it != mapkv.end()){
 8             sum += it->second;
 9         }
10     }
11     return sum;
12 }

  当程序中需要多次从map中查找元素时,每一次都需要查找并判断元素是否存在。再上面的代码中第7行判断元素是否存在时,每一次都需要调用end()函数获取map的结束迭代器。还有每次查找中,都需要创建一个迭代器来结束查找的值。这些地方都可以优化,优化如下:

 1 std::map<int, int> mapkv;
 2 int sum(std::vector<int> veckey){
 3     size_t size = veckey.size();
 4     int sum = 0;
 5     std::map<int, int>::iterator itend = mapkv.end();
 6     std::map<int, int>::iterator it = itend;
 7     for(size_t i = 0; i < size; ++i){
 8         it = mapkv.find(veckey[i]);
 9         if(it != itend){
10             sum += it->second;
11         }
12     }
13     return sum;
14 }

  在循环体外,先定义两个迭代器,并设置为map的结束迭代器,这样可以避免在循环体内中多次后去map的结束迭代器,提升效率。

3. 算法优化

  

3.1 全角字符查找

 

3.2 utf8字符字节数计算

 

3.3 数字字母字符判断

 

3.4 函数变量静态化

 

3.5 空间换时间

 

3.6 函数按“完美”数据实现逻辑处理

 

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网