cpu 告诉缓存行对齐(cache cline)

转载:http://blog.csdn.net/snowwalf/article/details/6784014

CPU的高速缓存一般分为一级缓存和二级缓存,现今更多的CPU更是提供了三级缓存。CPU在运行时首先从一级缓存读取数据,如果读取失败则会从二级缓存读取数据,如果仍然失败则再从内存中存读取数据。而CPU从一级缓存或二级缓存或主内存中最终读取到数据所耗费的时钟周期差距是非常之大的。因此高速缓存的容量和速度直接影响到CPU的工作性能。 一级缓存都内置在CPU内部并与CPU同速运行,可以有效的提高CPU的运行效率。一级缓存越大,CPU的运行效率往往越高。

       一级缓存又分为数据缓存和指令缓存,他们都由高速缓存行组成,对于X86架构的CPU来说,高速缓存行一般是32个字节,早期的CPU大约只有512行高速缓存行,也就是说约16k的一级缓存。而现在的CPU一般都是32K以上的一级缓存。

        当CPU需要读取一个变量时,该变量所在的以32字节分组的内存数据将被一同读入高速缓存行,所以,对于性能要求严格的程序来说,充分利用高速缓存行的优势非常重要。一次性将访问频繁的32字节数据对齐后读入高速缓存中,减少CPU高级缓存与低级缓存、内存的数据交换。

       但是对于多CPU的计算机,情况却又不一样了。例如:

1、      CPU1 读取了一个字节,以及它和它相邻的字节被读入 CPU1 的高速缓存。

2、      CPU2 做了上面同样的工作。这样 CPU1 , CPU2 的高速缓存拥有同样的数据。

3、      CPU1 修改了那个字节,被修改后,那个字节被放回 CPU1 的高速缓存行。但是该信息并没有被写入RAM 。

4、      CPU2 访问该字节,但由于 CPU1 并未将数据写入 RAM ,导致了数据不同步。

        当一个 CPU 修改高速缓存行中的字节时,计算机中的其它 CPU会被通知,它们的高速缓存将视为无效。于是,在上面的情况下, CPU2 发现自己的高速缓存中数据已无效, CPU1 将立即把自己的数据写回 RAM ,然后 CPU2 重新读取该数据。 可以看出,高速缓存行在多处理器上会导致一些不利。

 

         从上面的情况可以看出,在设计数据结构的时候,应该尽量将只读数据与读写数据分开,并具尽量将同一时间访问的数据组合在一起。这样 CPU 能一次将需要的数据读入。

 

如:

[cpp]  view plain  copy
  1. Struct __a  
  2. {  
  3.    Int id; // 不易变  
  4.    Int factor;// 易变  
  5.    Char name[64];// 不易变  
  6.    Int value;// 易变  
  7. } ;  

这样的数据结构就很不利。

在 X86 下,可以试着修改和调整它

[cpp]  view plain  copy
  1. Struct __a  
  2. {  
  3.    Int id; // 不易变  
  4. Char name[64];// 不易变  
  5. Char __Align[32 – sizeof(int) - sizeof(name)*sizeof(name[0])%32]  
  6.    Int factor;// 易变  
  7.    Int value;// 易变  
  8. Char __Align2[32 –2* sizeof(int)%32]  
  9. } ;  

32 – sizeof(int)+sizeof(name)*sizeof(name[0])%32

32 表示 X86 架构中缓存中,高速缓存行为 32字节 大小。 __Align 用于显式对齐。


再来一个有利于高速缓存行的例子:

[cpp]  view plain  copy
  1. struct CUSTINFO  
  2. {  
  3.    DWORD    dwCustomerID;    //Mostly read-only  
  4.    int      nBalanceDue;     //Read-write  
  5.    char     szName[100];     //Mostly read-only  
  6.    FILETIME ftLastOrderDate;  //Read-write  
  7. };  

改版后的结构定义 :

[cpp]  view plain  copy
  1. // Determine the cache line size for the host CPU.  
  2. //为各种CPU定义告诉缓存行大小  
  3. #ifdef _X86_  
  4. #define CACHE_ALIGN  32  
  5. #endif  
  6. #ifdef _ALPHA_  
  7. #define CACHE_ALIGN  64  
  8. #endif  
  9. #ifdef _IA64_  
  10. #define CACHE_ALIGN  ??  
  11. #endif  
  12.   
  13. #define CACHE_PAD(Name, BytesSoFar) \  
  14.    BYTE Name[CACHE_ALIGN - ((BytesSoFar) % CACHE_ALIGN)]  
  15.   
  16. struct CUSTINFO  
  17. {  
  18.    DWORD    dwCustomerID;     // Mostly read-only  
  19.    char     szName[100];      // Mostly read-only  
  20.   
  21.    //Force the following members to be in a different cache line.  
  22.    //这句很关键用一个算出来的Byte来填充空闲的告诉缓存行  
  23.    //如果指定了告诉缓存行的大小可以简写成这样  
  24.    //假设sizeof(DWORD) + 100 = 108;告诉缓存行大小为32  
  25.    //BYTE[12];  
  26.    //作用呢就是强制下面的数据内容与上面数据内容不在同一高速缓存行中。  
  27.    CACHE_PAD(bPad1, sizeof(DWORD) + 100);  
  28.   
  29.    int      nBalanceDue;      // Read-write  
  30.    FILETIME ftLastOrderDate;  // Read-write  
  31.   
  32.    //Force the following structure to be in a different cache line.  
  33.    CACHE_PAD(bPad2, sizeof(int) + sizeof(FILETIME));  
  34. };  

    高速缓存控制器是针对数据块,而不是字节进行操作的。从程序设计的角度讲,高速缓存其实就是一组称之为缓存行(cache line)的固定大小的数据块,其大小是以突发读或者突发写周期的大小为基础的。

    每个高速缓存行完全是在一个突发读操作周期中进行填充或者下载的。即使处理器只存取一个字节的存储器,高速缓存控制器也启动整个存取器访问周期并请求整个数据块。缓存行第一个字节的地址总是突发周期尺寸的倍数。缓存行的起始位置总是与突发周期的开头保持一致。

    现代处理器有专门的功能单元来执行加载和存储操作。加载单元每个时钟周期只有启动一条加载操作;与加载操作一样,在大多数情况下,存储操作能够在完整流水线化的模式中工作,每个周期开始一条新的存储。

    共享内存的并行计算,有可能会出现"伪共享"问题.比如,两个处理器各要访问一个word,这两个word却存在于同一个cache line大小的区域里,这时,从应用逻辑层面说,这两个处理器并没有共享内存,因为他们访问的是不同的内容(不同的word)。但是因为cache line的存在和限制,这两个CPU要访问这两个不同的word时,却一定要访问同一个cache line块,产生了事实上的“共享”。显然,由于cache line大小限制带来的这种“伪共享”是我们不想要的,会浪费系统资源。

更多请参考http://download.oracle.com/docs/cd/E19205-01/821-0393/aewcx/index.html


猜你喜欢

转载自blog.csdn.net/liguangxianbin/article/details/80491830