关于C#:L1 Cache Miss的成本是多少?

What is the Cost of an L1 Cache Miss?

编辑:为了参考目的(如果有人偶然发现这个问题),伊戈尔·奥斯特洛夫斯基写了一篇关于缓存未命中的伟大文章。它讨论了几个不同的问题,并显示了示例编号。结束编辑

我做了一些测试,想知道性能差异是否是由于内存缓存未命中造成的。下面的代码演示了这个问题,并将其归结为关键的计时部分。下面的代码有几个循环,它们以随机顺序访问内存,然后以升序访问地址。

我在一台XP机器(用vs2005:cl/o2编译)和一个Linux设备(gcc-os)上运行它。两者产生的时间相似。这些时间以毫秒为单位。我相信所有的循环都在运行并且没有被优化(否则它会"立即"运行)。

1
2
3
*** Testing 20000 nodes
Total Ordered Time: 888.822899
Total Random Time: 2155.846268

这些数字有意义吗?差异主要是由于一级缓存未命中还是其他原因造成的?有20000^2个内存访问,如果每一个都是缓存未命中,那么每一次未命中大约有3.2纳秒。我测试的XP(P4)机器是3.2GHz,我怀疑(但不知道)有32kb的一级缓存和512kb的二级缓存。对于20000个条目(80kb),我假设没有大量的二级未命中。所以这就是(3.2*10^9 cycles/second) * 3.2*10^-9 seconds/miss) = 10.1 cycles/miss。我觉得很高。也许不是,或者我的数学不好。我尝试用vtune测量缓存未命中,但我得到了一个bsod。现在我无法让它连接到许可证服务器(GRRRR)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
typedef struct stItem
{
   long     lData;
   //char     acPad[20];
} LIST_NODE;



#if defined( WIN32 )
void StartTimer( LONGLONG *pt1 )
{
   QueryPerformanceCounter( (LARGE_INTEGER*)pt1 );
}

void StopTimer( LONGLONG t1, double *pdMS )
{
   LONGLONG t2, llFreq;

   QueryPerformanceCounter( (LARGE_INTEGER*)&t2 );
   QueryPerformanceFrequency( (LARGE_INTEGER*)&llFreq );
   *pdMS = ((double)( t2 - t1 ) / (double)llFreq) * 1000.0;
}
#else
// doesn't need 64-bit integer in this case
void StartTimer( LONGLONG *pt1 )
{
   // Just use clock(), this test doesn't need higher resolution
   *pt1 = clock();
}

void StopTimer( LONGLONG t1, double *pdMS )
{
   LONGLONG t2 = clock();
   *pdMS = (double)( t2 - t1 ) / ( CLOCKS_PER_SEC / 1000 );
}
#endif



long longrand()
{
   #if defined( WIN32 )
   // Stupid cheesy way to make sure it is not just a 16-bit rand value
   return ( rand() << 16 ) | rand();
   #else
   return rand();
   #endif
}

// get random value in the given range
int randint( int m, int n )
{
   int ret = longrand() % ( n - m + 1 );
   return ret + m;
}

// I think I got this out of Programming Pearls (Bentley).
void ShuffleArray
(
   long *plShuffle,  // (O) return array of"randomly" ordered integers
   long lNumItems    // (I) length of array
)
{
   long i;
   long j;
   long t;

   for ( i = 0; i < lNumItems; i++ )
      plShuffle[i] = i;

   for ( i = 0; i < lNumItems; i++ )
      {
      j = randint( i, lNumItems - 1 );

      t = plShuffle[i];
      plShuffle[i] = plShuffle[j];
      plShuffle[j] = t;
      }
}



int main( int argc, char* argv[] )
{
   long          *plDataValues;
   LIST_NODE     *pstNodes;
   long          lNumItems = 20000;
   long          i, j;
   LONGLONG      t1;  // for timing
   double dms;

   if ( argc > 1 && atoi(argv[1]) > 0 )
      lNumItems = atoi( argv[1] );

   printf("

*** Testing %u nodes
"
, lNumItems );

   srand( (unsigned int)time( 0 ));

   // allocate the nodes as one single chunk of memory
   pstNodes = (LIST_NODE*)malloc( lNumItems * sizeof( LIST_NODE ));
   assert( pstNodes != NULL );

   // Create an array that gives the access order for the nodes
   plDataValues = (long*)malloc( lNumItems * sizeof( long ));
   assert( plDataValues != NULL );

   // Access the data in order
   for ( i = 0; i < lNumItems; i++ )
      plDataValues[i] = i;

   StartTimer( &t1 );

   // Loop through and access the memory a bunch of times
   for ( j = 0; j < lNumItems; j++ )
      {
      for ( i = 0; i < lNumItems; i++ )
         {
         pstNodes[plDataValues[i]].lData = i * j;
         }
      }

   StopTimer( t1, &dms );
   printf("Total Ordered Time: %f
"
, dms );

   // now access the array positions in a"random" order
   ShuffleArray( plDataValues, lNumItems );

   StartTimer( &t1 );

   for ( j = 0; j < lNumItems; j++ )
      {
      for ( i = 0; i < lNumItems; i++ )
         {
         pstNodes[plDataValues[i]].lData = i * j;
         }
      }

   StopTimer( t1, &dms );
   printf("Total Random Time: %f
"
, dms );

}


这里是一个尝试提供洞察缓存未命中的相对成本的类比烘烤巧克力片饼干…

你的手是你的寄存器。你花了1秒钟把巧克力片放到面团里。

厨房柜台是你的一级缓存,比寄存器慢12倍。12×1=12秒后走到柜台,拿起一袋核桃,把一些倒在手上。

冰箱是二级缓存,比一级慢四倍。走到冰箱前,打开冰箱,把昨晚剩下的东西移开,拿出一盒鸡蛋,打开纸箱,在柜台上放3个鸡蛋,然后把纸箱放回冰箱。

橱柜是你的三级缓存,比二级缓存慢三倍。3 x 48=2分24秒,走三步到橱柜,弯下腰,打开门,四处寻找烘焙供应罐,从橱柜中取出,打开,挖开找到烘焙粉,放在柜台上,把你洒在地板上的烂摊子扫干净。

主存储器呢?那是街角商店,比L3慢5倍。5 x 2:24=12分钟后,你就可以找到你的钱包,穿上你的鞋子和夹克,冲到街上,拿上一升牛奶,冲回家,脱下你的鞋子和夹克,回到厨房。

请注意,所有这些访问都是恒定的复杂性——O(1)——但是它们之间的差异会对性能产生巨大影响。纯粹针对大o复杂度进行优化,就像决定是一次向面糊1中添加巧克力片,还是一次添加10片,但忘记了将它们放在杂货店的清单上。

这个故事的寓意是:组织你的内存访问,使CPU不得不尽可能少地去买东西。

从CPU缓存刷新谬误博客帖子中获取的数字表明,对于特定的2012年Intel处理器,以下是正确的:

  • 寄存器访问=每周期4条指令
  • l1延迟=3个周期(12 x寄存器)
  • l2延迟=12个周期(4 x l1,48 x寄存器)
  • L3延迟=38个周期(3 x l2,12 x l1,144 x寄存器)
  • DRAM延迟=65 ns=3 GHz CPU上的195个周期(5 x L3、15 x L2、60 x L1、720 x寄存器)

处理器缓存效果库也可以很好地阅读此主题。

Mmmm, cookies ...


虽然我不能回答这些数字是否有意义(我不太熟悉缓存延迟,但对于记录的10个周期的一级缓存错过听起来是正确的),但我可以向您提供cachegrind作为一个工具来帮助您实际了解两个测试之间的缓存性能差异。

cachegrind是一个valgrind工具(支持始终可爱的memcheck的框架),用于描述缓存和分支命中/未命中。它将让您了解您在程序中实际获得了多少缓存命中/未命中。


对于一级缓存未命中,3.2ns是完全合理的。相比之下,在一个特定的现代多核PowerPC CPU上,一级未命中大约有40个周期——对于某些内核来说,要比其他内核长一些,这取决于它们与二级缓存的距离(是的,真的)。二级未命中至少为600个周期。

缓存是性能上的一切;CPU比内存快得多,现在您几乎要为内存总线而不是核心进行优化了。


是的,看起来这主要是一级缓存未命中。

一级缓存丢失的10个周期听起来是合理的,可能有点低。

从随机存取存储器中读取的数据将达到100秒,甚至可能是1000秒(我现在太累了,无法尝试做数学运算),所以它仍然是一个巨大的胜利。


如果您计划使用cachegrind,请注意它只是一个缓存命中/未命中模拟器。这并不总是准确的。例如:如果您访问某个内存位置,例如在循环中执行0x1234 1000次,那么cachegrind将始终显示只有一个缓存未命中(第一次访问),即使您有类似的情况:

循环中的clflush 0x1234。

在x86上,这将导致所有1000个缓存未命中。


Lavalys Everest跑步记录中3.4GHz P4的一些数字:

  • 一级数据缓存为8K(缓存线64字节)
  • L2是512K
  • 一级提取延迟为2个周期
  • 二级提取延迟大约是您看到的两倍:20个周期

这里更多:http://www.freeweb.hu/instlatx64/genuineintel0000f25_p4_gallatin_memlatx86.txt

(对于延迟,请查看页面底部)


在没有更多测试的情况下,很难确定要说什么,但是根据我的经验,差异的大小肯定可以归因于CPU L1和/或L2缓存,特别是在随机访问的情况下。通过确保每个访问至少与最后一个访问保持一定的最小距离,您可能会使情况变得更糟。


最简单的方法是拍摄目标CPU的缩放照片,并物理测量核心和一级缓存之间的距离。将该距离乘以电子每秒在铜中的移动距离。然后计算出在同一时间内可以有多少个时钟周期。这是一级缓存未命中将浪费的CPU周期的最小数目。

您还可以根据以相同方式浪费的CPU周期数,计算从RAM中获取数据的最低成本。你可能会惊讶。

请注意,您在这里看到的内容肯定与缓存未命中有关(无论是l1还是l1和l2),因为通常情况下,当您访问缓存线上任何需要较少的RAM访问的内容时,缓存将拉出同一缓存线上的数据。

然而,您可能也看到了这样一个事实:RAM(即使它称为随机存取存储器)仍然倾向于线性存储器存取。