L1caching小姐的成本是多less?

编辑 :为了参考的目的(如果有人绊倒这个问题),Igor Ostrovsky写了一个关于caching未命中的伟大的职位 。 它讨论了几个不同的问题,并显示示例编号。 结束编辑

我做了一些testing<long story goes here>并且想知道性能差异是否是由于内存caching未命中所致。 下面的代码演示了这个问题,并将其归结为关键的时间部分。 下面的代码有几个循环,以随机顺序访问内存,然后按升序地址顺序访问。

我在XP机器(用VS2005:cl / O2编译)和Linux机器上(gcc -Os)运行它。 两者产生了类似的时间 这些时间以毫秒为单位。 我相信所有循环都在运行,没有被优化(否则它会“立即”运行)。

 ***testing20000个节点
总有序时间:888.822899
总计随机时间:2155.846268

这些数字是否有意义? 主要是由于一级caching未命中还是其他事情呢? 有2万2 ^ 2的内存访问,如果每一个都是一个caching未命中,那就是每个小姐约3.2纳秒。 我testing的XP(P4)机器是3.2GHz,我怀疑(但不知道)有一个32KB L1caching和512KB L2。 有20,000个参赛作品(80KB),我认为没有大量的L2错失。 所以这将是(3.2*10^9 cycles/second) * 3.2*10^-9 seconds/miss) = 10.1 cycles/miss 。 这对我来说似乎很高。 也许这不是,或者我的math不好。 我试着测量与VTune的caching未命中,但我有一个BSOD。 现在我无法连接到许可证服务器(grrrr)。

 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( "\n\n*** Testing %u nodes\n", 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\n", 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\n", dms ); } 

虽然我不能提供数字是否有意义的答案(我不熟悉caching延迟,但是对于logging~10循环的L1caching未命中听起来是正确的),我可以为您提供Cachegrind作为工具来帮助您真正看到两次testing之间caching性能的差异。

Cachegrind是一个Valgrind工具(支持总是可爱的memcheck的框架),它可以对caching和分支命中/未命中进行configuration。 它会给你一个你在程序中实际获得多lesscaching命中/想法的想法。

这里尝试通过类似于烘焙巧克力cookies来提供洞察caching缺失的相对成本。

你的手是你的登记册。 需要1秒钟将巧克力片倒入面团。

厨房柜台是你的一级caching,比寄存器慢十二倍。 需要12×1 = 12秒才能到柜台,拿起核桃袋,把一些东西倒进你的手中。

冰箱是你的二级caching,比L1慢四倍。 它需要4 x 12 = 48秒才能走到冰箱,打开它,把昨晚的剩菜倒出来,拿出一盒鸡蛋,打开纸箱,把三个鸡蛋放在柜台上,然后把纸箱放回去冰箱。

橱柜是你的L3caching,比L2慢三倍。 需要3 x 48 = 2分24秒的时间,向橱柜走三步,向下弯曲,打开门,根部find烘烤供应锡,从橱柜中提取,打开它,挖掘find发酵粉,把它放在柜台上,把你洒在地板上的垃圾清理干净。

和主内存? 这是一个angular落店,比L3慢5倍。 需要5 x 2:24 = 12分钟才能find你的钱包,穿上你的鞋子和外套,冲上街头,拿起一升牛奶,冲回家,脱下你的鞋子和外套,然后回到厨房。

请注意, 所有这些访问都是“线性复杂度” – O(1) – 但它们之间的差异会对性能产生巨大的影响。 纯粹为了大O的复杂性而进行优化就像决定是一次一次地join巧克力片1还是10次一样,但是忘记将它们放在购物清单上。

道德的故事:组织你的记忆访问,所以CPU必须尽可能less去杂货。

数字来自CPU Cache Flushing Fallacy博客文章,这表明对于特定的2012年英特尔处理器,以下情况属实:

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

处理器caching效果图库也使这个主题很好的阅读。

嗯,饼干...

一个L1高速caching未命中3.2ns是完全合理的。 相比之下,在一个特定的现代多核PowerPC CPU上,L1未命中约为40个周期 – 对于某些内核来说,比其他内核稍长一些,这取决于它们离L2高速caching有多远(是的)。 L2缺失至less有 600个周期。

caching是性能上的一切; 现在的CPU比内存要快得多,所以你真的差不多是为内存总线而不是核心进行优化。

那么是啊,这看起来将主要是L1caching未命中。

一个L1高速caching未命中的10个周期听起来是合理的,可能有点偏低。

从RAM中读取的数据量大约是100s,或者甚至可能是1000s(现在我们已经厌倦了试图进行math计算),所以它仍然是一个巨大的胜利。

如果您打算使用cachegrind,请注意,它只是一个caching命中/未命中模拟器。 这并不总是准确的。 例如:如果你访问某个内存位置,在一个循环中说0x1234 1000次,cachegrind会一直告诉你只有一个caching未命中(第一次访问),即使你有类似的东西:

在你的循环中clflush 0x1234。

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

一些拉瓦利珠穆朗玛峰运行的3.4GHz P4的数字:

  • L1caching为8K(caching行64字节)
  • L2是512K
  • L1读取延迟是2个周期
  • L2读取延迟大约是您看到的两倍:20个周期

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

(关于延迟看看页面的底部)

没有太多的testing,很难说什么,但根据我的经验,差异的程度肯定可以归因于CPU L1和/或L2caching,特别是在随机访问的情况下。 通过确保每个访问与最后一个距离至less有一些距离,你可能会更糟。

最简单的做法是拍摄目标cpu的缩放照片,并物理测量核心与一级caching之间的距离。 通过电子每秒可以在铜中传播的距离乘以该距离。 然后找出在同一时间可以有多less个时钟周期。 这是L1caching未命中浪费的最lessCPU周期数。

你也可以用同样的方法计算从RAM中获取数据的最小开销。 你可能会惊讶。

注意你在这里看到的肯定与caching未命中有关(不pipe是L1还是L1和L2),因为一旦你访问该caching行上的任何内容,caching就会在同一个caching行上取出数据减less访问内存。

然而,你可能也看到的是,RAM(即使它被称为随机存取存储器)仍然更喜欢线性存储器访问。