多核编程伪共享问题及其对策
作者: Zhouweiming 周伟明 (42 篇文章) 日期: 三月 26, 2009 在 10:26 上午
多核编程中的伪共享问题及其对策
注:本文主要内容摘自笔者所著的《多核计算与程序设计》一书,略有修改,后续还会继续发布系列文章,如有需要,可以考虑将一下地址加入到您的浏览器收藏夹中:http://software.intel.com/zh-cn/blogs/category/multicore/。
前一篇文章 “多核编程锁竞争问题及其对策”中,提到了多核编程所遇到的各种问题,本文就来讲述一种的一个问题,伪共享问题及其对策。
伪共享问题在《多核程序设计技术-通过软件多线程提升性能》一书中有详细讲解,它是由于CPU cache机制造成的,CPU读取Cache时是以行为单位读取的,如果两个硬件线程的两块不同内存位于同一Cache行里,那么当两个硬件线程同时在对各自的内存进行写操作时,将会造成两个硬件线程写同一Cache行的问题,它会引起竞争,就像在乒乓球比赛一样,效率将成百倍的下降。
在单核系统中,伪共享问题是不存在的,因为同一时刻只有一个硬件线程在执行,不存在同时写同一Cache行的问题。
伪共享问题在实际情况中是经常可以碰到的,比如两个线程同时写一个数组的相邻部分,或者写两块相邻的内存,这些都有可能造成伪共享问题。
对于分配的内存,可以采取一定的内存分配算法使各块内存不在同一Cache行里,但对于数组或变量的访问,就必须要由程序员在设计时进行避免伪共享问题。
要解决伪共享问题,首先必须知道给定的内存中,那块区域会处于同一Cache行内,Intel的系统中,有一个简单的算法可以得到一块内存中对应的Cache行首地址,即每个Cache行首地址都是Cache行大小的整数倍。
比如一块内存大小为60字节,首地址为0x0012ff52,由于0x0012ff52除以64以后余数为0x12,因此这个地址不是Cache行的首地址。在这个地址之前的Cache行首地址为0x0012ff40,在这个地址之后的Cache行首地址为0x0012ff80。对应的内存位于两块不同的Cache中。如下图所示:
图1:Cache行对齐示意图
根据上面所说的Cache行首地址为Cache行大小的整数倍的特点,可以设计一个函数来取出给定地址之后的第0个Cache行首地址。代码如下:
/** 计算给定地址之后的第0个Cache行首地址
如果给定地址刚好为一个Cache行首地址,那么计算结果等于它自身
@param void *pAddr - 给定的地址
@return void * - 返回给定地址之后的第0个Cache行首地址
*/
void *GetCacheAlignedAddr(void *pAddr)
{
int m = CACHE_LINE_SIZE;
void *pRet = (void *)((UINT(pAddr + m - 1)) & (-m));
return pRet;
}
取到了Cache行首地址后,就可以区分出两块内存是否在同一Cache行中了,对避免伪共享问题就有了很大的帮助。
如果给定地址刚好等于某个Cache行首地址,但是却想取到的地址是它的下一个Cache行首地址,那么可以用以下函数来获取:
/** 计算给定地址之后的Cache行首地址
如果给定地址刚好为一个Cache行首地址,那么计算结果等于它的下一个Cache行首地址
@param void *pAddr - 给定的地址
@return void * - 返回给定地址之后的Cache行首地址
*/
void *GetNextCacheAlignedAddr(void *pAddr)
{
int m = CACHE_LINE_SIZE;
void *pRet = (void *)(((UINT)(pAddr) + m )&(-m));
return pRet;
}
当然,伪共享问题在许多地方都会出现,各种地方的处理方法各有不同,但是所有的方法都需要用到上面讲过的取Cache行首地址的方法。
伪共享问题遇得较多的地方是处理数组类型的数据,其次是内存管理上要从源头上将伪共享问题减少。后续的多核系列文章中,还会有文章详细讲解具体的伪共享处理实例。