C++ 性能优化之性能刺客
(给CPP开发者加星标,提升C/C++技能)
互斥锁的竞争,字符串的低效拷贝,算法的高复杂度等问题,通常我们都知晓它们的存在,关键点只在于我们要不要去花时间优化它们。但今天要说的主角,可不是那么容易被发现,人们常常形容它是性能刺客,它就是“伪共享”。
在讲伪共享之前,先带大家再复习一遍计算机缓存Cache和Cache Line。
Cache和Cache Line
我们都知道,在计算机存储体系中,离CPU越近的的存储器,存储性能越高,价格越贵,所以容量就越小。离CPU最近的是寄存器,然后是高速缓存,再是内存,再是磁盘。
Cache,中译名高速缓冲存储器,其作用是为了更好的利用局部性原理,减少CPU访问主存的次数。其实就是CPU会把经常要访问的数据和它附近的数据拷贝到Cache中,如果CPU下次取这些数据的时候就可以直接读Cache里的数据,而不是内存中的数据,这样访问的速度就快了很多。
Cache Line可以简单的理解为Cache中的最小缓存单位。内存和高速缓存之间或者高速缓存与高速缓存之间的数据移动最小单位就是Cache Line。目前主流CPU Cache的Cache Line大小都是 64Bytes。
查看 cache line 大小:
cat/sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size
CacheMiss:当运算器需要从存储器中提取数据时,它首先在最高级的cache中寻找然后在次高级的cache中寻找。如果在cache中找到,则称为命中hit;反之,则称为不命中miss。所以在代码中,遇到两重循环的情况,一般是把遍历元素多的循环放里面,防止cache 频繁更新,导致大量cache miss。
伪共享
接下来,进入正题,假设一个场景,有4个独立线程,分别访问一个int数组,数组有4个元素,第一个线程对第一个元素做++操作,第二个线程对第二元素做++操作,第三个第四个以此类推。这样操作不存在多线程问题,各自线程操作各自的变量,也不需要加锁了,看起来不会有什么性能问题了,代码如下:
void func(int* Num)
{
for(int i = 0; i < 100000; i++)
(*Num)++;
}
int main()
{
int ArrayTest[4];
uint64_t start,end;
thread t1,t2,t3,t4;
start = GetTimeStamp();
t1 = thread(func,& ArrayTest[0]);
t2 = thread(func,& ArrayTest[1]);
t3 = thread(func,& ArrayTest[2]);
t4 = thread(func,& ArrayTest[3]);
t1.join();
t2.join();
t3.join();
t4.join();
end = GetTimeStamp();
printf("Use no align time %lu us\n",end-start);
}
结合我们前面讲到的,cache line是内存到高速缓存的最小移动单位。不同线程的数据应该尽量放到不同的Cache Line,避免多线程修改同一行Cache,导致Cache需要在多核之间进行同步,降低性能。
以上程序的打印结果是:
Use no align time 1931 us
接下来,我们按照“不同线程的数据应该尽量放到不同的Cache Line”这个理论,改一下代码:
#define CACHE_LINE_SIZE 64
struct STest
{
alignas(CACHE_LINE_SIZE) int a;
};
void func(STest* test)
{
for(int i = 0; i < 100000; i++)
test->a++;
}
int main()
{
uint64_t start,end;
thread t1,t2,t3,t4;
STestArrayTest[4];
t1 = thread(func,& ArrayTest[0]);
t2 = thread(func,& ArrayTest[1]);
t3 = thread(func,& ArrayTest[2]);
t4 = thread(func,& ArrayTest[3]);
t1.join();
t2.join();
t3.join();
t4.join();
end = GetTimeStamp();
printf("Use align time %lu us\n",end-start);
}
打印结果是:
Use align time 736 us
可以看到,修改代码后,得到了超过两倍的性能提升。
C++11中引入了一个关键字alignas,即设置数据的对齐方式,本例中,按照64字节对齐(cache line的大小)。
总结:
避免伪共享:
(1)填充字节数
(2)修改变量对齐方式
伪共享优化意义:
如果在非高性能开发中,其实不用太关注它,因为强行设置对齐方式或者填充字节数会带来内存浪费,比如本来一个int的32个元素的数组,size也就128个字节,如果设置64字节对齐,那size就是2048字节了。不过在高性能开发当中,伪共享的优化就显得比较重要了。
参考
https://zhuanlan.zhihu.com/p/37069591
https://www.cnblogs.com/jokerjason/p/10711022.html
- EOF -
关于C++性能优化,欢迎在评论中和我探讨。觉得文章不错,请点赞和在看支持我继续分享好文。谢谢!
关注『CPP开发者』
看精选C++技术文章 . 加C++开发者专属圈子
↓↓↓
点赞和在看就是最大的支持❤️