程序员老鸟说不用纠结性能优化?错!如何日常开发中编写性能良好的代码?

掌握性能优化技巧,在平时养成编写性能良好程序的习惯,远比你想象中重要

”程序员浪费了太多的时间去思考和担忧程序中那些非关键部分的速度,而且考虑到调试和维护,这些为优化而进行的修改实际上是有很大负面影响的。我们应当忘记小的性能改善,97% 的情况下,过早优化都是万恶之源。“ ——高德纳《使用 goto 语句进行结构化编程》

很多程序员都或多或少地看过或表达过类似的话语,甚至以此作为座右铭。实际上,绝大多数人误解了高德纳博士的意思。高德纳博士的本意是没有必要纠结于小的性能改善,但并不代表程序员可以以此为借口,写出性能极差的代码,对软件性能造成损害,而且——是在本可以保证开发速度的情况下。掌握一些编程性能上的一些关键诀窍,并不会对开发进度造成太多的影响(实际开发中,甚至可能是减少了开发时间。你的产品和leader未必满意你写的shit)。“过早优化都是万恶之源“并不能作为程序员偷懒不学习,保持坏的编程习惯的理由。

另外一方面,用户对软件性能体验的要求,是持续上升,甚至加速上升的。随着摩尔定律的失效,编译器优化逐渐到达尽头,软件性能的重担又将从编译器底层开发程序员,体系结构和CPU工程师肩膀上,逐渐回到应用开发程序员身上。以我最近的工作为例,我所参与开发的某大型国产办公软件,在以往的十几年一直被吐槽卡慢,不如微软的某办公软件。但也就是在今年,由于对性能层面的用户反馈比例增大的厉害,大佬们才从研发层面正式组织团队研究性能优化专项,下定决心解决这个心头之患。用户对软件性能体验优化日益增长的需要,以及摩尔定律失效和应用开发程序员编程水平之间的矛盾,显然在逐渐增大。

顺带一提,TIOBE编程社区评选出2022年度编程语言,是C++。在2022年,TIOBE指数统计中Java的分数被C++反超。除了近年来C++11/14/17/20标准的确立和普及,给C++带来了新的生命力之外,TIOBE CEO Paul Jansen 评价称,C++ 受欢迎的原因在于它作为一种高级面向对象语言的出色性能。由此可见,不管是使用任何编程语言,在任何操作系统平台上开发任何业务,性能的重要性,在未来的占比会越来越高。


TIOBE编程社区2022年编程语言排位

因此,掌握性能优化技巧,在平时养成编写性能良好程序的习惯,远比你想象中重要。”高质量发展“将是可预见未来种任何领域的主旋律。作为程序员,若不能成为”高质量发展“的一部分,后果可想而知。

日常开发中,C++程序员如何编写性能良好程序?

性能优化中,有一些指导经验是通用的,我们可以问一问:能否使用更好的编译器?更好的算法和数据结构?更好的库/更合理的使用库?如何减少程序中发生的动态内存分配/复制?哪一段程序的性能是关键的,如何找到它们?本文章不会展开论述,有兴趣可以参考文章末尾的推荐阅读。
尽然,并不是所有的程序员都有机会参与到性能优化的专项工作中。但是,养成良好的编程习惯,在日常开发中编写性能良好的代码,这对减少产品/leader/客户的指指点点和扯皮,减少返工重写的概率,增加加薪升职的期望,还是十分有帮助的。下面便是笔者认为在日常开发中提高代码性能的一些key tips。这里代码以C++为案例,但相信对其他语言的开发者也有一定帮助。

Tips No.1 :标准库中的字符串使用动态内存分配,性能开销很大。

std::string类,包括类似于cocos,Qt等三方库提供的字符串,比C风格的字符串确实好用得多。但,everything has a price,方便带来的便是性能的下降。这主要原因在于,为了实现字符串最大长度的灵活可变,且不浪费内存,C++风格字符串使用了动态内存分配,相当于底层有一个指向自由存储区的char*指针,程序员在操作字符串时,为了保证动态分配的内存足够使用,C++风格字符串会自动的进行动态内存的分配和释放,而这种内存的开辟和释放,只能由程序运行时阶段确定大小。对于编译器来说,这对编译期优化带来了困难,而const char*这种C风格字符串,则是编译期便可确定内存大小,编译器有更大的优化空间。

针对这个核心思想,我们能做的有:尽量不要定义全局的C++风格字符串对象。这往往是影响程序启动性能的一个大坑。除此之外,也尽量不要定义全局的类对象,除非十分有必要。像能够使用基本类型替代的情况,尽量使用基本类型(编译器对基本类型的优化比较好做)。

#include<...>

//No!
//const std::string APP_NAME = "jiu_zhuan_da_chang";
//Yes!
constexpr char* APP_NAME = "jiu_zhuan_da_chang";

int main()
{
...
}

字符串连接运算符的开销是很大的。它会调用内存管理器去构建一个新的临时字符串对象来保存连接后的字符串。相比之下,复合赋值操作符 +=的消耗少得多。

std::string strA = "strA";
std::string strB = "strB";
//No!
//strA = strA + strB;
//Yes!
strA += strB;

为字符串预留内存空间可以减少内存分配的开销。这对std::vector等长度可以动态增长的数据结构同样适用。若字符串预期长度比较长,且能够大概知道目标大小,则可以使用revserve方法预分配一定长度,避免字符串对象内部多次动态申请和释放内存。

//去除控制字符(ASCII值小于0x20)
std::string remove_ctrl_reserve(std::string s) {
    std::string result;
    result.reserve(s.length());
    for (int i = 0; i < s.length(); i++)
    {
        if (s[i] >= 0x20)
            result += s[i];
    }
    return result;
}

对于非基本类型的参数,若不会在函数逻辑中修改,都应该使用const引用的方式传入,减少复制构造函数产生的开销。这对C++风格字符串以外的类同样适用。

//参数声明为const引用
void funcA(const std::string& s) ;
void funcB(const ClassA& a);

函数结果可以使用参数传引用的方式返回给调用者,避免函数return时产生的额外一次复制,进而导致动态内存分配。

//处理结果位于result中。这种情况可以返回一个布尔值,标识是否调用成功。工程上这或许是更好的惯用法
bool funcA(const std::string& input, std::string& result) ;

减少对字符串的非必要转换。一是不同字符串风格的转换,有的情况下可以使用const char*替代std::string以及其他字符串类,有必要使用C++风格字符串时再进行转换即可;二是不同编码的转换,尽量在一个程序中使用同一种字符串编码。

//No!这里返回值会构造一个std::string,而调用者可能只是用char*去接这个返回值,外层又要从std::string转回char*
//std::string MyClass::Name() const {
//  return "MyClass";
//}
//Yes!
const char* MyClass::Name() const {
    return "MyClass";
}

C++17提供了std::string_view数据结构,std::string_view涵盖了std::string的所有只读接口。如果生成的std::string无需进行修改操作,可以把std::string转换为std::string_view,或者直接使用字符串字面量初始化std::string_viewstd::string_view记录了对应的字符串指针和偏移位置,无需管理内存,相对std::string拥有一份字符串拷贝,如字符串查找和拷贝,效率更高。

std::string_view str_view_str = "testing string_view related..";

对于性能特别敏感的情况下,可以使用C风格字符串,编译器可以更好的进行优化。此外,有时候函数参数使用const引用可能会导致性能反降,因为引用本身就是指针的封装,函数中若对声明为const引用的参数高频率访问会产生多次的指针解引用,导致额外的性能消耗。不过一般来说,这两种情况都比较少见,后者可以使用迭代器的方式减少指针解引用或者视场景优化。除非测试发现参数传引用的开销更大,否则应保持传const引用的惯用法。

Tips No.2:运行时内存分配开销不小

如前面字符串的内容说到,动态变量有运行时开销,编译器难以优化。而不少C++程序员在声明类成员变量和局部变量时,都可能存在声明和创建动态变量,而这有时候没有必要。(而且这也很危险,不用智能指针包裹时容易导致内存泄漏)。除非有必要,否则静态地创建变量即可。

MyClass* myInstance = new MyClass("hello", 123); //可能是非必要地动态创建
MyClass myInstance("hello", 123); //静态创建类示例即可

class ClassA
{

private:
    //No!可能是非必要的变量动态创建
    //std::shared_ptr<ClassB> m_pB = nullptr;
    //Yes!
    ClassB m_b;
};

此外,养成使用C++11的智能指针的习惯固然是好事,不过注意std::shared_ptr由于实现能够共享动态变量的所有权,所以使用原子性的引用计数增减进行动态内存管理,而原子操作往往开销不小。若没有共享动态内存的需要,优先使用std::unique_ptr,是更明智的选择。

//std::shared_ptr<MyClass> myClass= nullptr;
std::unique_ptr<MyClass> myClass= nullptr;

在创建共享所有权的智能指针,应使用std::make_shared()模板函数。传入new表达式生成的动态变量构造std::shared_ptr,会导致调用两次内存管理器,一次用于创建 yClass的实例,另一次用于创建被隐藏起来的引用计数对象。而std::make_shared()能够做到只调用一次内存管理器。

//No!
//std::shared_ptr<MyClass> p(new MyClass("hello", 123));
//Yes!
std::shared_ptr<MyClass> p = std::make_shared<MyClass>("hello", 123);

有时候使用静态的数据结构,能够达成更好的性能。C++11引入了std::array,它是固定大小的字符串。如果在编译时能够知道数组的大小,或是最大的大小,那么可以使用与 std::vector 具有类似接口,但数组大小固定且不会调用内存管理器的std::array

在循环中重复进行不必要的动态内存分配也是很糟糕的一种情况。如无必要,在循环外定义可能进行动态内存分配的变量。

//No!config字符串反复动态分配内存
//for (auto& filename : namelist) {
//  std::string config;
//  ReadFileXML(filename, config);
//  ProcessXML(config);
//}

//Yes!调用clear方法不会释放config字符串预分配的动态内存区域
std::string config;
for (auto& filename : namelist) {
    config.clear();
    ReadFileXML(filename, config);
    ProcessXML(config);
}

C++11引入了移动语义完美转发特性的支持。移动语义解决的问题包括:

  • 将一个对象赋值给一个变量时,会导致其内部的内容被复制。这个运行时开销非常大。而在这之后,原来的对象立即被销毁了。复制的努力也随之化为乌有,因为本来可以复用原来对象的内容的。
  • 开发人员希望将一个实体(即不应支持复制的变量),例如一个 unique_ptr 或是资源处理句柄,赋值给一个变量。在这个对象中,赋值语句中的“复制”操作是未定义的,因为这个对象具有唯一的识别符。

因此,设计可能发生较多运行时内存分配的类和接口时,声明移动构造函数,使用移动语义和完美转发特性对减少不必要的运行时内存分配十分有帮助。建议早日掌握。

当一个数据结构中的元素被存储在连续的存储空间中时,我们称这个数据结构为扁平的,如std::arraystd::vector。相对于list、map、unordered_map等非扁平数据结构,更少地使用内存管理器进行动态内存分配,且缓存局部性更好。

Tips No.3:一些惯用的算法优化模式

预计算是一种常用的技巧,通过在程序执行至热点代码之前,先提前进行计算来达到从热点代码中移除计算的目的。预计算有多种不同的形式,既可以将计算从热点代码移至程序中不那么热点的部分,也可以移动至程序链接时、编译时和设计时。

int sec_per_day = 60 * 60 * 24;//常量表达式可以编译期优化
int sec_per_day = sec_per_min * min_per_hour * hour_per_day;//编译器难以优化

延迟计算的目的在于将计算推迟至更接近真正需要进行计算的地方。延迟计算带来了一些好处。如果没有必要在某个函数中的所有执行路径(if-then-else 逻辑的所有分支)上都进行计算,那就只在需要结果的路径上进行计算。其中两段构建(two-part construction)是延迟计算的一种策略。两段构建是指类特别的定义一个初始化函数,让类使用者可以在合适的时候再进行初始化,以减少一些不必要场合下的初始化开销(可能根本不会用这个类)。

class MyClass
{
public:
    MyClass();
    void init();
private:
    std::string m_str;
};

MyClass::MyClass()
{
    //不做任何初始化工作
}

void MyClass::init()
{
    //真正的初始化工作
    m_str = "hello world";
}

批量处理的目标是收集多份工作,然后一起处理它们。批量处理可以用来移除重复的函数调用或是每次只处理一个任务时会发生的其他计算。当有更高效的算法可以处理所有输入数据时,也可以使用批量处理将计算推迟至有更多的计算资源可用时。举例如下。

  • 缓存输出是批量处理的一个典型例子。输出字符会一直被缓存,直至缓存满了或是程序遇到行尾(EOL)符或是文件末尾(EOF)符。相比于为每个字符都调用输出例程,将整个缓存传递给输出例程节省了性能开销。
  • 将一个未排序的数组转换为堆的最优方法是通过批量处理使用更高效算法的一个例子。将 n 个元素一个一个地插入到堆中的时间开销是 O(n log2n),而一次性构建整个堆的开销则只有 O(n)。
std::vector<int> vec{ 6, 1, 2, 5, 3, 4 };
std::make_heap(vec.begin(), vec.end()); //6 5 4 1 3 2
  • 多线程的任务队列是通过批量处理高效地利用计算资源的一个例子。一些任务通过任务队列定时地批量处理,往往可以减少计算开销。
  • 全局常量如需动态初始化,且耗时久(比如表达式中调用复杂non-constexpr 函数,或类常量初始化时调用复杂构造函数),建议通过函数调用来替代全局变量以延迟初始化,函数实现通过局部静态变量来避免多次调用。
const int g_var = func1();
...
func2(g_var);

//Yes!
int getVar()
{
    static int s_var = func1();
    return s_var;
}
...
func2(getVar());

缓存指的是通过保存和复用昂贵计算的结果来减少计算量的方法。这样可以避免在每次需要计算结果时都重新进行计算。经典的场景是动态规划,懂的都懂。而线程池等工程策略,则是缓存了一些创建开销比较大的系统资源。在业务开发中,一些字符串长度之类的计算和获取,可以存到变量中,尤其在循环逻辑中时,反复获取字符串长度(已知循环不会改变字符串长度)会造成不必要的开销。

特化与泛化相对。特化的目的在于移除在某种情况下不需要执行的昂贵的计算。还是以字符串为例,std::string 可以动态地改变长度,容纳不定长度字符的字符串。它提供了许多操作来操纵字符串。如果只需要访问字符串,那么使用 C 风格的数组或是指向字面字符串的指针会更加高效,或者使用std::string_view

使用提示来减少计算量,可以达到减少单个操作的开销的目的。例如,std::map中有一个重载的 insert() 成员函数,它有一个表示最优插入位置的可选参数。最优提示可以使插入操作的时间开销变为 O(1),而不使用最优提示时的时间开销则是 O(log2n)。

优化期待路径。在有多个 else-if 分支的 if-then-else 代码块中,如果条件语句的编写顺序是随机的,那么每次执行经过 if-then-else 代码块时,都有大约一半的条件语句会被测试。如果有一种情况的发生几率是 95%,而且首先对它进行条件测试,那么在 95% 的情况下都只会执行一次测试。

双重检查是指首先使用一种开销不大的检查来排除部分可能性,然后在必要时再使用一个开销很大的检查来测试剩余的可能性。举例如下。

  • 当比较两个字符串是否相等时,通常需要对字符串中的字符逐一进行比较。不过,首先比较这两个字符串的长度可以很快地排除它们不相等的情况。
  • 双重检查可以用于散列法中。首先比较两个输入数据的散列值,可以高效地判断它们是否不相等。如果散列值不同,那么它们肯定不相等。只有当散列值相等时才需要逐字节地进行比较。

Tips No.4 循环和多次被调用的函数,是热点代码

我们可以通过在进入循环时预计算并缓存循环结束条件值。这和上面提到的缓存惯用法是一致的。根据测试,示例代码的加速可以达到20倍。(当然主要是因为循环体的语句耗时较少,结束条件的计算耗时较高)

char s[] = "This string has many space (0x20) chars. ";
//No!
//for (size_t i = 0; i < strlen(s); ++i)
//{
//  if (s[i] == ' ')
//      s[i] = '*';
//}
//Yes!
for (size_t i = 0, len = strlen(s); i < len; ++i)
{
    if (s[i] == ' ')
        s[i] = '*';
}

循环中具有循环不变性的语句,可以移动到循环开始之前。注意,一些语句隐含了函数调用,包括类的构造,析构,赋值,操作符重载等函数,也具有开销。

void rotate(std::vector<Point>& v, double theta) {
    for (size_t i = 0; i < v.size(); ++i) {
        double x = v[i].x_, y = v[i].y_;
        v[i].x_ = cos(theta) * x - sin(theta) * y;
        v[i].y_ = sin(theta) * x + cos(theta) * y;
    }
}
void rotate_invariant(std::vector<Point>& v, double theta) {
    //不必反复调用sin(theta)和cos(theta)
    double sin_theta = sin(theta);
    double cos_theta = cos(theta);
    for (size_t i = 0; i < v.size(); ++i) {
        double x = v[i].x_, y = v[i].y_;
        v[i].x_ = cos_theta * x - sin_theta * y;
        v[i].y_ = sin_theta * x + cos_theta * y;
    }
}

对于被反复调用的虚函数,可以考虑在链接时选择接口实现,避免虚函数的运行时开销。假如需要支持跨平台的文件读写接口,那么在不同平台下链接不同的cpp文件是一种选择。许多程序员在这种场景下,会将接口声明为virtual,并继承出windowsFile和linuxFile,在子类中重写接口。这会带来额外的开销。

// file.h——接口
class File {
public:
    File();
    bool Open(Path& p);
    bool Close();
    int GetChar();
    unsigned GetErrorCode();
};

// windowsfile.cpp——Windows的实现代码
# include "File.h"
bool File::Open(Path& p) {
    //windows api
}
bool File::Close() {
    //windows api
}

// linuxfile.cpp——Linux的实现代码
# include "File.h"
bool File::Open(Path& p) {
    //linux api
}
bool File::Close() {
    //linux api
}

或者在编译时选择接口实现,也就是使用编译器给出的预定义宏判断当前系统平台。两种方式都可以避免访问虚函数表的开销。

// file.cpp——实现
# include "File.h"
# ifdef _WIN32
 bool File::Open(Path& p) {
 ...
 }
 bool File::Close() {
 ...
 }
 ...
# else // Linux
 bool File::Open(Path& p) {
 ...
 }
 bool File::Close() {
 ...
 }
 ...
# endif

一些类成员函数不会访问类的私有成员数据,也不会调用其他的非静态成员函数。那么,使用静态成员函数取代成员函数,能够减少this指针的隐式传入,减少开销。

整形计算比浮点数计算要快得多,如无必要尽量使用整形计算。另外,在一些平台上,双精度类型可能会比浮点型更快。

最后,循环和反复调用的函数中中是否有可以移除的语句,这就需要视业务场景判断。

Tips No.5:优化STL的使用

C++ 标准库中,有几种不同的算法都实现了二分查找,比如std::binary_search()std::equal_range(),和std::lower_bound()。实测发现,std::lower_bound()的性能最优。

kv* result = std::lower_bound(std::begin(names), std::end(names), key);
//没找到,result设置为指向表末尾的迭代器。否则,它会返回键等于 key 的元素。
if (result != std::end(names) && key < *result.key)
    result = std::end(names);

std::deque是一种专门用于创建“先进先出”(FIFO)队列的容器。在队列两端插入和删除元素的开销都是常量时间。下标操作也是常量时间。它的迭代器与std::vector一样,都是随机访问迭代器,因此对std::deque进行排序的时间开销是 O(n log2n)。std::vector在前端插入元素的速度相比后端插入元素,由于会引发数据的内存复制,速度慢非常多。但在进行后端插入、删除、遍历和排序操作时std::vector都是最快的容器。除非有前插的需要,否则使用std::vector是更好的选择。Kurt Guntheroth测试发现,对于相同数量的元素,vector的赋值操作的性能是 deque 的 13 倍,删除操作的性能是 deque 的 22 倍,基于迭代器的插入操作的性能是 deque 的 9 倍,push_back() 操作的性能是 deque 的两倍,使用 insert()在末尾插入元素的性能则是 deque 的 3 倍。

甚至,直接用std::list替代std::deque,或许是更好的选择。测试发现,将元素插入到 list 末尾的开销不足 vector 的两倍。遍历和排序 list 的开销只比 vector 多了 30%。大部分操作,std::list都比std::deque的效率更高。从底层设计的角度来看,std::deque不像std::vector一样把所有对象保存在一个连续的内存块,而是多个连续的内存块,这会带来更频繁的动态内存操作和访问开销(获取std::deque的元素需要访问两次指针引用)。

上文提到的检查并更新惯用法,在std::mapstd::multimap中要格外注意。map::find()map::insert()的时间开销都是 O(log2n)。这两种操作都会遍历 map 的二叉树数据结构中的相同的节点。

int key = 1;
int value = 10;
std::multimap<int, int> table;
auto it = table.find(1); // O(log n)
if (it != table.end()) {
    // 找到key的分支
    it->second = value;
}
else {
    // 没有找到key的分支
    table.insert(std::pair<int, int>(key, value)); // O(log n)
}

//推荐使用
auto it2 = table.lower_bound(key);
if (it2 == table.end() || key < it2->first) {
    // 找到key的分支
    table.insert(it2, std::pair<int, int>(key, value));
}
else {
    // 没有找到key的分支
    it2->second = value;
}

std::mapstd::multimap的查找问题上,如果要一次性构造一个含有100 000 条元素的表并会反复对其进行查找,那么使用vector实现会更快(vector构造后,排序,后续使用二分查找)。当然,如果表中保存的元素会频繁地发生改变,例如对表进行插入操作或是删除操作,那么重排序基于vector的表可能会抵消它原本在查找性能上的优势。

std::unordered_map的性能相对std::map更优异,尤其是在查找上。

Other tips

处理文件读写等IO相关操作时,将数据一次性读取到内存中,再进行业务逻辑,完成后再写回文件,往往能减小读写开销。std::istream 提供了一个read()成员函数,它能够将字符直接复制到缓冲区中。此外,将文件内容读取到作为缓冲区的std::string之前,读取文件流的大小并一次预开辟足够大的空间,能进一步减小开销。

std::streamoff stream_size(std::istream& f) 
{
    std::istream::pos_type current_pos = f.tellg();
    if (-1 == current_pos)
        return -1;
    f.seekg(0, std::istream::end);
    std::istream::pos_type end_pos = f.tellg();
    f.seekg(current_pos);
    return end_pos - current_pos;
}

bool stream_read_string(std::istream& f, std::string& result) 
{
    std::streamoff len = stream_size(f);
    if (len == -1)
        return false;
    result.resize(static_cast<std::string::size_type>(len));
    f.read(&result[0], result.length());
    return true;
}

此外,写文件时,std::endl会刷新输出,回写到硬盘文件中。事实上,大多数时候只需要在必要时刷新即可。

//每次都会刷新到硬盘文件
void stream_write_line(std::ostream& f, std::string const& line)
{
    f << line << std::endl;
}

//由调用者手动调用f.flush()或关闭流对象,才刷新输出
void stream_write_line_noflush(std::ostream& f,
    std::string const& line)
{
    f << line << "\n";
}

调用std::ios_base:: sync_with_stdio(false)可以关闭C++输入输出流和C标准库流的同步,改善性能。

C++11提供了std::thread作为并发的支持,但从 C++ 标准实现来看,std::async() 可能是使用线程池的方式实现的。在 Windows 上,std::async()明显快得多。std::thread每次调用都会启动一个新的线程,而创建线程是会有开销的,包括操作系统为线程在操作系统的表中分配空间的开销、为线程的栈分配内存的开销、初始化线程寄存器组的开销和调度线程运行的开销。因此,除了使用std::async()作为异步任务的处理方式以外,使用Windows的IOCP机制,Qt的QThreadPool类等方式使用线程池,是更高效的并发编程解决方案。

工程实践中,创建处理大量任务线程数量,也就是连续计算的可运行线程建议不超过核心数量,否则并不会提高效率,反而增加了线程同步,线程调度等方面的开销。一个连续计算的可运行线程会消耗运行它的核心的 100% 的计算资源。如果有 n 个核心,那么在每核心都运行一个可运行线程能够将时钟运行时间减少到几乎 1/n。不过,一旦在每个核心上都有一个线程正在运行,那么即使再增加额外的线程也无法进一步缩短运行时间,反而会将CPU的使用时间划分为小之又小的碎片。C++ 提供了std::thread::hardware_concurrency() 函数,来返回可使用的CPU核心数。

在多线程程序中,线程可能会竞争资源。任何时候,两个或多个线程需要相同的资源时,互斥都会导致线程挂起,无法并发。解决竞争问题的关键,包括注意内存和 I/O 都是资源,避免过多线程访问内存和IO;可以复制资源,比如一些数据可以让每一个线程都拥有一份非共享的副本,来移除多线程对于共享的 map 或是散列表等资源的竞争;或者让线程只访问所需要的那一小部分数据,避免直接的竞争;使用细粒度锁,即我们可以使用多个互斥量,而不是一个互斥量来锁住整个数据结构;使用读写锁,无锁数据结构等技巧。

对于基本数据类型,初始化的性能开销是 0。链接器会让变量指向初始化数据。但是对于具有静态存储期的类类型,初始化过程会以标准所指定的特定顺序,在单独的线程中连续地调用各个变量的构造函数。因此,尽量不要创建全局的类变量,尤其是字符串对象。

References & 推荐阅读:
《C++性能优化指南》——Kurt Guntheroth
《Effective Modern C++ 》——Scott Meyers

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • C++、C#、Java调用C++ DLL 来源:https://www.jianshu.com/p/a425b65...
    盗生一_52b5阅读 95评论 0 0
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,447评论 1 51
  • 写在前面: 犹豫了一下,不知道该不该发这么多。毕竟题目虽全,但是其实很多人看了不到一半,估计就会默默的收藏保存,等...
    Yt_cc阅读 14,510评论 1 6
  • 1. C++中使用空对象指针调用成员函数 空指针对Test_Fun1和 Test_Fun2的调用正常,对Test_...
    Bystander_1996阅读 460评论 0 0
  • 本文转自:https://www.cnblogs.com/lancidie/archive/2011/08/05/...
    Nrocinu阅读 1,295评论 0 1