Cache Concurrency Problem False Sharing

刚刚看到MSDN上一篇文章提到false sharing problem。以前从没注意过,这里做个笔记,作为备忘。False Sharing并不会导致数据不一致,但却可能严重影响并发性能。

背景知识

现代CPU有一种机制,能够保证多个CPU之间的缓存数据一致。比如说,有两个线程分别运行在两个CPU上,它们都读取了变量A,存储在各自的CPU的cache里。之后线程1改写了A的值,为了减少访问内存带来的性能损失,此时CPU1并不会立即把A的新值写入内存,而是暂时保存在缓存中。同时CPU1监听系统总线,一旦发现其他CPU需要读取A所在缓存块的值,CPU1就会把A的新值从总线发送出去,同时将这个值写入内存。这样能保证一个cache里做的更改,能及时同步到另一个cache。
另外,CPU在缓存数据的时候,通常不只缓存当前这一个值,而是会把它周围的值都缓存起来,称作一个cache line (具体大小取决于CPU),因为通常当内存中的一个值被访问到的时候,很可能它周边的值将会陆续被用到,比如函数内的局部变量,再比如进行诸如遍历数组这类操作,采用这样的预读机制就可以大大减少将来的访存次数。作为比较,文章举了一个二维数组遍历的例子,同样一个二维数组,如果先按列遍历(内循环为a[0][i]..a[n][i]),会比先按行遍历慢很多倍。 

问题描述

这两种机制看起来都很不错,但当它们同时发生作用的时候,就可能产生问题。

举例说,有两个线程分别运行在两个CPU上,它们分别读取了数组内相邻的两个元素a[0],a[1],并分别对它们进行独立的操作,也即,线程1只读写a[0],线程2只读写a[1]。

但是请注意,因为CPU是按块缓存内存的,所以a[0]和a[1]会同时存在于两个CPU的内存中,虽然每个CPU其实只用到了其中1个值。这样问题就来了。假设线程1改变了a[0]的值,而线程2此时要读取a[1]的值,显然从代码逻辑上来讲,此时缓存是不需要同步的,但CPU按照前述机制一的逻辑,会判断a[0]所在缓存块内的值被读取了,所以需要同步,于是把a[0]的值写入内存,发生了一次不必要的访存操作。这样的情况就被称为False Sharing。显然,如果这种情况大量存在的话,程序性能将大大降低。

解决办法

避免False Sharing的办法也很直观,那就是尽量让各个线程之间的数据在存储位置上拉开距离。比如上面的例子,我们可以让线程1访问a[0],但把线程2用到的数据挪到a[i],其中a[0]和a[i]的地址之差恰好大于一个缓存块大小。这样问题就避免了。

但判断程序中是否存在False Sharing却不是那么直观,主要还是需要观察程序运行期间的一些统计数据。具体可以参考开头提到的那篇文章,Methods for Detecting False Sharing 和 Using the Visual Studio Profiler 部分。

原文地址:https://www.cnblogs.com/k330/p/2281151.html