zoukankan      html  css  js  c++  java
  • C/C++程序CPU问题分析

    转载地址:http://www.10tiao.com/html/473/201606/2651473094/1.html

      程序的CPU问题是另外一类典型的程序性能问题,很多开发人员都受到过程序CPU占用过高的困扰。本次我们收集了14个CPU类的问题,和大家一起分析下这些问题的种类和原因。另外,对于C/C++程序而言,目前已经有了很多CPU问题定位的工具,本文也会进行比较分析。

    CPU问题分析

      程序CPU类问题的主要现象是:程序占用的CPU过高,比程序升级前有很大的升高。导致程序CPU占用过高的主要原因是程序设计不合理,绝大部分的CPU问题都是程序设计的问题。因此,提高程序的设计质量是避免CPU问题的主要手段。

    1.1 大量低效操作引起的问题

    在程序设计中,有些程序的写法是比较低效的,没有经验的同学很容易使用一些低效的函数或方法,这就是我们常说的“坑”。我们搜集到了一些“坑”,跟大家分享下。

    memset是一个很常见的性能坑。如果在程序中使用的memset过多,会导致程序的CPU消耗很大。memset使用过多,往往在不经意间就让程序下降了一大截。关于memset函数,一种常见的误用是在循环中对较大的数据结构进行memset。在这个例子中,一个query中memset 1M的内容,在整体1500qps的情况下,每秒进行重置的内存达到1.5G,导致程序的CPU IDLE答复下降。

    上面这个例子,是memset一种比较明显的问题,通过代码review等方式是比较容易发现的。在一些情况下,memset操作是在隐式发生的,问题的排查难度也随之加大。

    #代码片段1
    char buffer[1024] = {0};
    • 1
    • 2

    代码片段1中的简简单单的一行代码,其实在实际的运行过程中是会调用memset的。这个就是一个坑:在栈内存中申请缓冲区,然后再赋值,会隐式的调用memset,将内存初始化为0。这个问题也导致了一个产品线的核心模块性能大幅下降,引起了严重的性能问题。

      另外,在使用一些系统函数或库函数时,也需要仔细阅读使用手册,避免出现大量的无效的内存申请、释放和重置操作。

    代码片段2
    memset(&t_data->preq, 0, sizeof(pusrinfo_req_t));
    memset(&t_data->pres, 0, sizeof(pusrinfo_res_t));
    
    odb_renew(t_data->cur_field_dict);
    • 1
    • 2
    • 3
    • 4
    • 5

    代码片段2中的这段代码中,第1、2行中的memset会导致程序的CPU使用过多,但即使是将这两行的代码注释掉,程序的性能依然没有明显的改观。问题的根源在于代码片段2中最后一行代码调用的odb_renew函数有释放内存和大量的memset操作,导致消耗的CPU很多。如果在程序中调用了大量的odb_renew函数,其性能一定不太好。

      strncpy这个字符串操作函数是比较耗费性能的,同strncpy函数实现类似功能的函数有snprintf和memcpy+strlen这两种方式。表1是在一台测试机上对这三种方式的性能比较。 
    这里写图片描述
    从表1可以看出,memcpy的性能最好。令人欣喜的是snprintf在大数据下性能渐渐逼近memcpy。稍微看了一下几个函数的源代码,memcpy用了page copy和word copy结合,所以性能优化的比较好,而且strlen也是用4字节做循环步长的。strncpy只是简单地逐字节拷贝,并且会将目标buffer后面所有的空闲空间全部填为0,这在很多情况下是非常耗费性能的。

      整体上,对于这类问题的主要解决方法是:识别CPU消耗多的函数并且尽量减少这类函数的使用。比如,有些数据结构的memset是没有必要的,这些数据结构会被下一个query的数据自然填充。又或者采用更高效的初始化的方法。典型的例子是,字符串数组的初始化,只需要将第一个字符设置为0即可。

    1.2 容器使用不当引起的问题

      程序设计中,容器的使用是必不可少的。不同类型的容器,其设计的目的是不同的,因此某些方面的性能天然地会比较低。我们在程序设计的时候,要能够正确的识别容器各种用法的性能,减少低效的使用。 
    这里写图片描述
    代码片段3中的第6行代码,将计算列表长度的方法放到了循环中,本身list类型求取长度的函数复杂度就是O(n),在这个操作放到循环中以后,直接将这段代码的复杂度提高到了O(n2),在列表中元素较多的情况下,对程序的性能将产生非常大的影响。代码片段3中的例子2是另外一种错误用法,算法复杂度也是O(n2)。 
    这里写图片描述

      上面的两个例子,还有一个典型的特点,就是存在循环。对于循环程序来说,要尽量避免在循环体内进行大量消耗CPU的操作。即使是每次消耗的CPU较少,但是由于存在循环,算法复杂度提升了一个数量级,因此要特别的小心。

    1.3 锁&上下文切换过多引起的问题

    &meps; 程序中存在过多的加锁/解锁操作,是程序CPU性能恶化的另外一大类原因,其典型的现象是:系统态的CPU过高,甚至超过了用户态CPU。 
      自旋锁和互斥锁一样,是常见的解决系统资源互斥的方法。与互斥锁不同,自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。一般情况下,自旋锁锁定的资源释放的都比较快,在这种情况下,由于调用者不需要睡眠,减少了系统的切换,因此可以提高程序的性能。但随着程序处理能力、流量、数据大小的变化,自旋锁有时也会导致程序性能恶化。在我们收集的一个百度知道的案例中,程序访问cache的时候,通过自旋锁进行同步的控制。程序刚开始上线时并无问题,但随着流量的增大,当程序的qps达到1700时,系统态CPU高达73%,自旋锁引起了严重的性能瓶颈。对于这个case,主要的解决方案就是要“去锁”,减少锁操作。 
      另外一个例子也是关于加锁过多的。凤巢检索端的一个模块,处理一个请求时,每次最多可以得到4096个词,程序需要获取这些词的信息,这些信息大部分是存储在cache中,如果cache中不存在,则需要重新计算并更新cache。曾经一个存在的问题是,程序的设计逻辑是每次从cache中查询一个词的信息,并且在查询cache时要进行加锁/解锁操作。那么一次请求,最多要进行4096次操作。对于一个qps达到1000的程序来说,每秒的加锁操作达到了百万级,程序的性能严重恶化。 
      在操作系统课程中我们学习过,当线程需要等待一定的条件时会被操作系统放入的休眠队列中,直到被唤醒。程序的上下文切换过多,也会导致程序的性能恶化。曾经在在一个模块中出现这样的现象,机器的系统态CPU出现周期性的增长,现象如图1所示。 
    这里写图片描述 
      经过排查,发现引起这种现象的原因是代码片段5中的一行shell代码引起的。这行代码的作用是将日志中含有keyword的最后100条日志找出来,并进行重新写数据。这行代码会被周期性的执行。图2给出了代码执行过程的示意图。grep写标准输出还经过标准C库这么一层缓冲,缓冲区大小默认是4K,也就是说grep先调用fwrite写标准C库缓冲区,写满4K以后,标准C库调用write系统调用将标准C库缓冲区刷到内核中的管道缓冲区,然后tail进程调用read系统调用从内核中的管道缓冲区一次性读取4K字节。很明显,grep写满内核中管道缓冲区以后,必须等待tail读取完成,才能继续写,那么这个时候,它就要被切换出去, 进入一个等待队列,tail进程被切换进来,读取4K字节,然后唤醒grep,tail被切换出去,grep被切换进来……随着需要grep的文件越来越大,进程切换的次数也越来越多,系统态的CPU占用也水涨船高。 
    这里写图片描述

    1.4 其他问题分析

      还有很多情况,都可能导致程序的CPU消耗过多,比如I/O操作过多。I/O操作过多问题中最常见的一类是程序打印了过多的日志。曾经在后羿系统就发生这样的例子,由于程序输出的日志从二进制升级为了字符串,整体的I/O量增加了30%,导致程序的吞吐量从3.8万降低到了2.1万,几乎下降一半。还有一个典型的I/O问题是程序中有很多的DEBUG日志,虽然最终在线上没有开启DEBUG日志打印,但是程序在运行过程中还是会走到DEBUG日志相关的程序逻辑,只是不进行日志的输出。如果在日志输出的地方,存在复杂的计算逻辑,那么程序的性能也下降。代码片段6中的代码就是一个例子。在这个debug日志输出的过程中调用了material对象的to_string函数,而这个函数非常的消耗性能。尽管程序最终没有输出DEBUG日志,但是to_string函数还是被调用到了,程序的性能依然会受到影响。 
    这里写图片描述

    Fast JSON是阿里巴巴提供的开源JSON工具,支持对JSON的序列化和反序列化的功能,号称是最快的JSON解析工具,在百度电影的部分模块中使用了这个工具。Fast JSON的1.2.2版本存在调用java.lang.System.getProperty时,多线程需要加锁,会带来线程hang住,引起系统性能降低的问题。这个问题导致了电影的这个模块出现了比较严重的线上问题。

    CPU问题定位工具比较分析

    对于C/C++程序,目前业界使用的比较多的CPU热点定位工具有:valgrind中组件callgrind,gprof(GNU Profiler),google perf tools组件中的CPU Profiler和Oprofiler。

    • callgrind工具(valgrind套件之一):valgrind整体采用虚拟机的解决方案,将被测程序的指令转换了valgrind自身的代码Ucode,这样就可以实现对被测程序全面的分析(CPU, MEM)。

    • gprof(GNU Profiler)工具 : GNU提供的工具,已经存在了30年左右了。主要通过在函数入口处插入代码的方式来统计函数的调用关系、次数及CPU使用方式。

    • google perf tools(CPU Profile):对程序的调用栈进行采样分析,通过调用栈反推出函数的调用次数、关系和CPU消耗时间。

    • Oprofile :利用cpu硬件提供的性能计数器,通过技术采样,从进程、函数、代码层面分析性能问题。更多的用于分析系统层面个的问题,用户态cpu只是其中一部分。

    在c++ perf tools初体验这篇文章中,有比较详细的各类工具的用法和原理说明,有兴趣的同学可以深度阅读。

    表2从多个维度对这4种工具进行了比较,综合比较这些因素后,我还是推荐使用google perf tools套件中的CPU Profiler,这个工具在灵活性、应用性等方面的优势非常明显。但就像表格中提到的,这种工具会让程序一定概率core dump。 
    这里写图片描述

    总结

    本文收集并分析了十几个C/C++程序CPU性能问题,通过对这些问题的分析,我们发现CPU相关的性能问题,很多都是由于程序设计问题引起的。减少低效的调用,充分释放CPU的能力,是提升程序CPU性能的关键。从更大的层面上来看,程序的CPU性能还需要更好的架构设计,充分调用各种资源来高效地完成任务。google perf tools套件中的CPU Profiler工具是一个非常优秀的定位CPU热点的工具,希望大家能够多用这类工具来优化程序的CPU。

  • 相关阅读:
    【Anagrams】 cpp
    【Count and Say】cpp
    【Roman To Integer】cpp
    【Integer To Roman】cpp
    【Valid Number】cpp
    重构之 实体与引用 逻辑实体 逻辑存在的形式 可引用逻辑实体 不可引用逻辑实体 散弹式修改
    Maven项目聚合 jar包锁定 依赖传递 私服
    Oracle学习2 视图 索引 sql编程 游标 存储过程 存储函数 触发器
    mysql案例~tcpdump的使用
    tidb架构~本地化安装
  • 原文地址:https://www.cnblogs.com/lidabo/p/10253502.html
Copyright © 2011-2022 走看看