C++ 性能优化那些事儿

By | 2017-05-14

最近在折腾一些性能优化的事儿, 顺便记录一下

当然, 烂大街的抄来抄去最没意思了, 这里一如既往的列一些不常见的状况

分支预测

这个其实是个挺有意思的话题, 这里不细讲, 有兴趣请先自行搜索 "分支预测" 学习一下

典型示例是: 对一个已排序的数组做 if else 遍历, 明显快于一个未排序的数组

不过通常比较少遇到上述情形, 这边列一个经常能碰到的细节:

// 示例1
T func() {
    if(cond) {
        return a;
    }
    return b;
}

// 示例2
T func() {
    if(cond) {
        return a;
    }
    else {
        return b;
    }
}

通常情况下, 示例2 比示例1 要快 (大约 5%~10%),
因为如果不在 else 中, return b; 将作为独立语句, 不参与分支预测

dynamic_cast 与虚函数

通常情况下, 虚函数调用只比普通函数调用略微慢一点 (多了一个虚表间接寻址的步骤)

不过 dynamic_cast 就不同了, 往往需要遍历继承树, 尤其在多继承和虚继承的时候, 性能相当糟糕

某些对性能要求高的场合, 如果能够明确继承关系和父子类的逻辑关系, 就需要各种姿势绕开 dynamic_cast 了

  • 姿势1

    class Base {
        virtual ChildA *toChildA(void) {return NULL;}
        virtual ChildB *toChildB(void) {return NULL;}
    };
    class ChildA : public Base {
        virtual ChildA *toChildA(void) {
            return this;
        }
    };
    class ChildB : public Base {
        virtual ChildB *toChildB(void) {
            return this;
        }
    };

    比较通用, 几乎没啥副作用和坑, 不过用起来不是很灵活, 适用于父类明确知道子类类型的情况,
    典型场景: 网络模块中各种 Message 类型

  • 姿势2:

    Child *child = isChildType(base) ? static_cast(base) : NULL;

    灵活但是坑也很大, isChildType 本身就有很多学问, static_cast 更是有讲究不能随便用的,
    典型情况就是多继承中不能直接使用, 有兴趣的可以去了解下 C艹 的对象内存布局, 就明白了

    典型: Qt 中的 qobject_cast, 这部分实现的源码还是比较值得一看的 (需要很多额外的辅助代码)

    ZFObjectCast
    采用的也是同样原理

间接寻址

这是个看上去简单易懂但实际上学问非常大的性能优化问题

  • 表象: 多一次寻址操作

    T *p = new T();
    T obj;
    
    p->func(); // 实际相当于 (*p).func();
    obj.func(); // 通常情况下比上一行略快
  • 深层次: 内存交换, CPU 缓存命中

    要理解这些, 还得先了解一些编译原理和硬件架构, 这边简单科普一下:

    • 一段程序要执行, 基本上得经历以下过程: 硬盘 > 内存 > CPU 缓存 > 寄存器 > 运算单元

      速度从慢到快, 但容量也从大到小, 容量级别大约为:

      硬盘 (TB/GB) > 内存 (GB/MB) > CPU 缓存 (1MB~10MB) > 寄存器 (寄存器个数 * CPU字长)

    • 为了性能, 上述每个过程之间基本上都有缓存

    • 缓存没命中的时候, 就必需: 删除旧的 > 从更慢一级的存储介质中读取

    • 此外, 硬件架构通常还有以下限制

      • 不能越级读取,
        例如 CPU 缓存必须从内存中读取, 如果想要硬盘中的数据, 必须先从硬盘读取到内存中,
        再从内存中读取
      • 读取时通常只能读取连续的一段数据, 分页读取

    懂了上述这些, 再回去看上面的例子

    • T obj; : 局部变量, 编译器很容易和函数执行代码等优化为一段连续的数据,
      很容易一次性装入 CPU 缓存中
    • new T() 或者从别的全局变量中取地址 : 对象存储的位置大多数情况下远离函数内容所在的地址,
      访问时容易涉及到缓存失效, 造成换页 (通常是 CPU 缓存未命中)

    当然, 即使知道了这些, 间接寻址的性能优化问题依旧是玄学, 并没有什么万用的方法,
    为了灵活性牺牲性能也是常有的事, 最简单粗暴的典型是, 把连续的指针访问替换为对象引用访问:

    T *p = xxx;
    p->a();
    p->b();
    p->c();
    
    // 替换为
    T *p = xxx;
    T &ref = *p;
    ref.a();
    ref.b();
    ref.c();

总结

上述提到的大多还是比较少遇到的情况, "不要过早优化" 依旧是真理,
而且大多数情况下, 性能优化都是一个权衡问题, 好好遵循二八原则,
把有限的性能优化的预算放到那 20% 的重要点上,
才是最适合的方式

转载请注明来自: http://zsaber.com/blog/p/170

既然都来了, 有啥想法顺便留个言呗? (无奈小广告太多, 需审核, 见谅)

Category: C++

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注