• 程序性能优化


      在硬件资源昂贵的时代,编程人员非常注重程序的性能,以期望用尽可能少的硬件资源完成尽可能多的事情。随着科技的发展,
    摩尔定律的魔力使得硬件资源已越来越便宜,速度也越来越快,似乎性能已不是编程人员所需关注的事情了。然而在一个竞争
    与发展的时代,软件的功能越来越复杂,用户的操作体验越来越重要,而且竞争越来越激烈,谁能以更优势的价格,更好的操
    作体验,完成更多更复杂的事情,谁就将在激烈竞争中胜出。因而软件的性能优化必将一直是软件领域所要关注的内容之一。
    虽然软件的性能优化贯穿了设计与编码的整个过程,本文也将从设计与编码两个层次对性能优化进行分析。本文还将从CPU、内
    存、磁盘、网络四个方面描述性能问题分析的过程。

    2.设计出来的性能
    1)系统架构
    控制流与数据流?减少不必要的模块
    2)程序结构
    多线程程序
    锁的粒度、各种锁/信号量的性能对比
    共享内存通信
    降低灵活性以获取高性能。
    减少不必要的重复判断(SHTTP/HTTP)
    3)接口设计
    好的接口给予使用者充分的灵活性
    4)数据结构与算法
    Linux内存管理,数量小时使用链表

    3.编码的艺术
    1)内存访问与文件
    减少new/delete或malloc/free操作减少换页
    减少文件打开与关闭操作
    减少文件读写次数(减少系统调用)
    2)减少不必要的运算
    消除重复运算
    循环中的运算
    最忙的循环放在里面
    3)语言及库函数特性的利用
    if与case语句
    构造与析构
    宏与内联函数
    迟缓型计算
    减少临时变量
    缓存字符串的长度
    不必要的memset
    4)硬件特性的利用
    字节对齐
    移位与乘除2
    性能热点用汇编实现
    4.性能分析工具-callgrind
    valgrind系列工具因为免费,所以在linux系统上面最常见。callgrind是valgrind工具里面的一员,它的主要功能是模拟cpu的cache,能够计算多
    级cache的有效、失效次数,以及每个函数的调用耗时统计。
    callgrind的实现机理(基于外部中断)决定了它有不少缺点。比如,会导致程序严重变慢、不支持高度优化的程序、耗时统计的结果误差较大等,
    更多的外部工具有oprofile,gprof,tprof,Rational Quantify and Intel VTune
    5.编译器参数的优化
    大家要记住的是,编译器绝对比想象的要强大的多。编写编译器的人大都是十年、几十年代码编写经验的科学家!你能简单想到的,他们都已经想
    到过了。普通的编译器,可以支持大部分已知的优化策略以及多媒体指令。至于哪个编译器更好,大部分人的观点是:intel。Intel毕竟是最优秀的
    cpu提供者,他们的编译器考虑了很多cpu的特性,跑的更快。但目前intel编译器有一些比较弱智的地方,即它只识别自己的cpu,不是自己的cpu,
    就认为是最差的i386-i686机器,从而不能在amd等平台上面支持sse功能。我们在linux上面写代码,一般更加喜欢流行的编译器,比如gcc。
    Gcc的优点是它更新快,开源,bug修改迅速。正因为他更新快,所以它能够支持部分C03的规范。
    5.1 gcc支持的优化技术
    1) 函数内联
    函数调用的过程是:压入参数到堆栈、保护现场、调用函数代码、恢复现场。当一个函数被大量调用的时候,函数调用的开销特别巨大。函数内
    联是指把这些开销都去除,而直接调用代码。函数内联的不好之处是难以调试,因为函数实际上已经不存在了。
    2) 常量预先计算
    a = b + 1000 * 8
    对于这段代码,程序会预先计算b + 1000 * 8,从而变成:
    a = b+ 8000
    3) 相同子串提取
    a=(b+1)*(b+1)
    这里,b+1需要计算2次,可以只用计算一次:
    tmp=b+1
    a=tmp*tmp
    4) 生存周期分析
    这是一个比较高级的技术。假设有代码:
    a=b+1
    c=a+1
    在执行的时候,因为第二句依赖第一句,所以2句是线性执行。
    但编译器其实可以知道,c就是等于b+2,所以代码变成:
    a=b+1
    c=b+2
    这样,这2句就没有任何关系来了,执行的时候,cpu可以并行执行它们。
    5) 清除跳转
    看如下代码:
    int func()
    {
    int ret = 0;
    if(xxx)
    ret=5;
    else if(yyy)
    ret=6;
    return ret;
    }
    当条件xxx满足的时候,程序还会跳到下面执行,但其实是没有必要的。编译器会把它变成:
    int func()
    {
    if(xxx)
    return 5;
    else if(yyy)
    return 6;
    }
    6) 循环展开
    循环由几个部分组成:计数器赋值、计算器比较、跳转。每次循环,后面2步都是必须的消耗。把循环内的代码拷贝多份,可以大大减少
    循环的次数,节约后面2步的耗时。参考:
    for(int counter = 0; counter < 4; count++)
    xxx;
    可以变成:
    xxx;
    xxx;
    xxx;
    xxx;
    编译器不仅仅可以展开普通循环,它还能展开递归函数。原理是一样的,递归其实是一个不定长的借用了堆栈的循环。
    7) 循环内常量移除
    for(int idx=0;idx<100;idx++)
    a[idx]=a[idx]*b*b;
    因为b*b在循环体内的值固定(常量),所以代码可以变成:
    tmp=b*b;
    for(int idx=0;idx<100;idx++)
    a[idx]=a[idx]*tmp;
    8) 并行计算
    大家都知道,现代cpu支持超流水线技术,同时可以执行多条语句。多条语句能否同时执行的限制是不能互相依赖。编译器会自动帮我们把
    看起来单线程执行的代码,变成并行计算,参考:
    d=a+b;
    e=a+d+f;
    可以变成:
    tmp=a+f;
    d=a+b;
    e=d+tmp;
    9) 表达式简化
    当年笔者在学习《离散数学》和《数字电路》的时候,总被眼花缭乱的布尔运算简化题目难倒。gcc终于让我松了一口气。参考:
    !a && !b
    这句需要3步执行,但变成:
    !(a || b)
    只需要2步执行。
    5.2 gcc重要优化选项
    1) 内联
    -finline-small-functions
    内联比较小的函数。-O2选项可以打开。
    -findirect-inlining
    间接内联,可以内联多层次的函数调用。-O2选项可以打开。
    -finline-functions
    内联所有可以内联的函数。-O3选项可以打开。
    -finline-limit=N
    可以进行内联的函数的最小代码长度。注意,这里是伪代码,不是真实代码长度。伪代码是编译器经过处理后的代码。带inline等标志的函数,默认
    300行代码即可内联,不带的默认50行代码。和这个相关的选项是max-inline-insns-single和max-inline-insns-auto。
    max-inline-insns-recursive-auto
    内联递归函数时,函数的最大代码长度。
    large-function-insns、large-function-growth、large-unit-insns等
    函数内联的副作用是它导致代码变多,程序变长。这里的几个参数可以控制代码的总长度,避免编译后出现巨大的程序,影响性能和浪费资源。
    2) -fomit-frame-pointer
    不采用标准的ebp来记录堆栈指针,节省了一个寄存器,并且代码会更短。但据说在某些机器上面会导致debug模式出错。实际测试表明,在gcc4.2.4以
    下,O2和O3都无法打开这个选项。
    3) -fwhole-program
    把代码当做一个最终交付的程序来编译,即明确指定了不是编译库。这个时候,编译器可以使用更多的static变量,来加快程序速度。
    4) mmx/ssex/avx
    多媒体指令,主要支持向量计算。一般来说,-march=i686、-mmx、-msse、-msse2是目前机器都支持的指令。
    除了基本的多媒体支持外,gcc编译器还支持-ftree-vectorize,这个选项告诉编译器自动进行向量化,也是-O3支持的选项。
    多说几句。在平常的使用中,多媒体指令不是很常见(除非游戏)。如果你有几个位表(bitset),它们需要进行各种位操作的话,多媒体指令还是挺有效果滴。
    5.3 gcc大杀器-profile driven optimize
    这是比较晚才出现的技术。其基本原理是:根据实际运行情况,缩短hot路径的长度。编译器通过加入各种计数器来监控程序的运行,然后根据计算出来
    的实际访问路径情况,来分析hot路径,并且缩短其长度。根据gcc开发者的说法,这种技术可以提高20-30%的运行效率。
    其使用方式为:
    编译代码,加上-fprofile-generate选项
    到正式环境一段时间
    当程序退出后,会产生一个分析文件
    利用这个分析文件,加上-fprofile-use,重新编译一次程序
    举个例子来说:
    a=b*5;
    如果编译发现b经常等于10,那么它可以把代码变成:
    a=50;
    if(b != 10)
    a=b*5;
    从而在大多数情况下,避免了乘法消耗。
    5.4 gcc支持的优化属性(__attribute__)
    aligned
    可以设置对齐到64字节,和cpu的cache line看齐
    fastcall
    如果函数调用的前面2个参数是整数类型的话,这个选项可以用寄存器来传递参数,而不是用常规的堆栈
    pure
    函数是纯粹的函数,任何时刻,同样的输入,都会有同样的输出。可以很方便依据概率来优化它。
    5.5 gcc其他优化技术
    #pragma pack()
    对齐到一个字节,节省内存
    __builtin_expect
    直接告诉编译器表达式最可能的结果,方便优化
    编译带debug信息的小文件
    以下代码能够大大减少编译后程序大小,同时保留debug信息。其原理是外链一个带debug的版本。
    g++ tst.cpp -g -O2 -pipe
    copy a.out a.gdb
    strip --strip-debug a.out
    objcopy --add-gnu-debuglink=a.gdb a.out
    6.算法是核心
    算法是程序的核心,一个程序的好坏,大部分是看起算法的好坏。对于一般的程序员来讲,我们无法改变系统和库的调用,只能根据
    规则来使用它们,我们可以改变的是我们自己核心代码的算法。
    算法能够十倍甚至百倍的提高程序性能。如快排和冒泡排序,在上千万规模的时候,后者比前者慢几千倍。
    通常情况下,有如下一些领域的算法:
    A)常见数据结构和算法
    B)输入输出
    C)内存操作
    D)字符串操作
    E)加密解密、压缩解压
    F)数学函数
    总上所述:性能问题通常体现在四个方面:CPU、内存、磁盘、网络几个方面。解决方法可以是修改代码甚至程序结构以更充分的利用现有资源,
    也可以是增加相应的硬件以增加资源供给。

  • 相关阅读:
    Python调用C++的DLL
    Go-map
    Go-切片
    Go-数组
    Go-流程控制
    Go-运算符
    Go-变量和常量
    Go-VS Code配置Go语言开发环境
    Go-跨平台编译
    Go-从零开始搭建Go语言开发环境
  • 原文地址:https://www.cnblogs.com/wenrenhua08/p/3941208.html
Copyright © 2020-2023  润新知