本文所使用的环境是 Ubuntu 14.04 32bit 系统, Intel I5 处理器, X86 体系结构
如果我说下面的程序存在性能问题,你信吗?
#include<thread> int32_t global[2]={0}; void f() { for(int i = 0; i < 100000000; i++) { ++global[0]; } } void g() { for(int i = 0; i < 100000000; i++) { ++global[1]; } } int main() { std::thread thread1(f); std::thread thread2(g); thread1.join(); thread2.join(); return 0; }
这个程序,在我的电脑上,运行时间为:
real 0m0.822s user 0m1.596s sys 0m0.000s
有人说,两个线程分别操作不同的计数器,这么完美的程序,会有性能问题?
答案是:有。
恩,原因在于大名鼎鼎的 false sharing 。如果您看过我以前写的这篇 博客,应该还记得:现在的计算机一般由一个内存、一个 CPU 组成,而包含多个 CPU Cores 和 Cache 。如这幅图所示:
cacheline是 cache 块单位,一个 cacheline 大小一般在 32 到 256 字节左右。 cacheline 是这张图中不同模块的数据交互元素。
在上面程序中, global 是两个 32 字节变量构成的数组,大小为 64 字节,很可能被放到同一个 cacheline 里。当运行在 CPU1 Core 上的线程 thread1 修改了 global[0] 时,会让运行在 CPU2 Core 上对应 global[0] 和 global[1] 的 cacheline 失效,因此运行在 CPU2 Core 上的线程 thread2 修改 global[1] 时会发生 cache miss ,接着它访问内存,修改 global[1] ,这也会让 CPU1 Core 中的 cacheline 失效。很明显,这里面会有大量的 cache miss 和为了缓存一致性而花费的通信开销。
因此这种 false sharing 发生在多核、多线程环境中。单核或者单线程不会有 false sharing 问题。
遗憾的是,程序里存在这样的问题,并不容易通过肉眼发现。
幸运的是,这种问题一旦知道,就比较好解决。
解决方法一:让这两个计数器间隔足够大,让它们不要在同一个 cacheline 里,不就行了么?
恩,定义一个 global[10000] ,然后线程 1 利用 global[0] ,线程 2 利用 global[9999] ,应该就可以了。
什么?这么愚蠢的方法都想得出来?接着往下看。
解决方法二:假如 global 不是一个数组呢?而是包含多个变量的结构体呢(这种情形也很常见)?上面的方法就不灵了吧?
恩,上面的方法不灵了,而且上面的方法太笨。网上有很多资料告诉你怎么定义变量让其 cacheline aligned ,这也是那些博客千篇一律的做法。还有没有其他方法?有。接着往下看。
解决方法三:重点来了。
我们其实可以在线程里使用局部变量!
#include<thread> int32_t global[2] = {0}; void f() { int counter1 = 0; for(int i = 0; i < 100000000; i++) { ++counter1; } global[0] = counter1; } void g() { int counter2 =0; for(int i = 0; i < 100000000; i++) { ++counter2; } global[1] = counter2; } int main() { std::thread thread1(f); std::thread thread2(g); thread1.join(); thread2.join(); return 0; }
counter1和 counter2 在自己的线程栈上, cacheline 位于对应的 CPU core 里,大家相安无事。只有执行第 9 行和第 17 行时代价可能高点。
这个程序,在我的电脑上运行时间为:
real 0m0.293s user 0m0.580s sys 0m0.000s
解决方法四:
global神马变量?全局变量。 counter1/counter2 什么变量?局部变量。
有没有一种东东,既有全局的性质,又有局部的效果(线程私有)呢?
恩,如果您看过我以前写的这篇 博客,就不会对__thread感到陌生。对!提供强大 scalability 的利器,就是它了!
#include<thread> __thread int counter = 0; void f() { for(int i = 0; i < 100000000; i++) { ++counter; } } void g() { for(int i = 0; i < 100000000; i++) { ++counter; } } int main() { std::thread thread1(f); std::thread thread2(g); thread1.join(); thread2.join(); return 0; }
这个程序在我的电脑上的运行时间为:
real 0m0.325s user 0m0.644s sys 0m0.000s
不过其他线程如何读取到每个计数线程的 counter 呢?不难,但是也不是那么简单,背后牵扯到很多问题(其实本文最大的目的是通过 false sharing 牵扯出 partition 这种并发编程里最大的设计原则)。我们下次专门聊。
1,编译以上多线程程序的时候,请使用:
g++ -pthread -std=c++11 xxx.cpp
如果没有指定 -pthread
,那么程序可以编译链接通过,运行时报错:
terminate called after throwing an instance of ‘std::system_error’
what(): Enable multithreading to use std::thread: Operation not permitted
Aborted (core dumped)
2,程序计时我用的是 time ./a.out
的方式。