75

编辑:出于参考目的(如果有人偶然发现这个问题),Igor Ostrovsky 写了一篇关于缓存未命中的精彩文章。它讨论了几个不同的问题并显示了示例编号。 结束编辑

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

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

*** 测试 20000 个节点
总订购时间:888.822899
总随机时间:2155.846268

这些数字有意义吗?差异主要是由于 L1 缓存未命中还是还有其他原因?有 20,000^2 次内存访问,如果每个都是缓存未命中,则每次未命中大约为 3.2 纳秒。我测试的 XP (P4) 机器是 3.2GHz,我怀疑(但不知道)有 32KB L1 缓存和 512KB L2。对于 20,000 个条目 (80KB),我假设没有大量的 L2 未命中。所以这将是(3.2*10^9 cycles/second) * 3.2*10^-9 seconds/miss) = 10.1 cycles/miss。这对我来说似乎很高。也许不是,或者我的数学不好。我尝试用 VTune 测量缓存未命中,但我得到了一个 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 );

}
4

8 回答 8

69

这里试图通过与烘焙巧克力曲奇的类比来深入了解缓存未命中的相对成本......

你的手就是你的寄存器。将巧克力片放入面团中需要 1 秒钟。

厨房柜台是您的一级缓存,比寄存器慢十二倍。走到柜台前需要 12 x 1 = 12 秒,拿起一袋核桃,然后倒一些到你的手中。

冰箱是您的 L2 缓存,比 L1 慢四倍。走到冰箱前需要 4 x 12 = 48 秒,打开它,把昨晚的剩菜移开,取出一盒鸡蛋,打开纸盒,将 3 个鸡蛋放在柜台上,然后将纸盒放回冰箱。

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

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

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

故事的寓意:组织你的内存访问,这样 CPU 就必须尽可能少地去买杂货。

数字取自CPU Cache Flushing Fallacy博客文章,该文章表明对于特定的 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 寄存器)

处理器缓存效果库也很好地阅读了这个主题。

嗯,饼干...

于 2015-03-21T21:56:28.813 回答
26

虽然我无法回答这些数字是否有意义(我并不精通缓存延迟,但据记录,大约 10 个周期 L1 缓存未命中听起来是正确的),我可以为您提供Cachegrind作为工具可帮助您实际查看 2 个测试之间的缓存性能差异。

Cachegrind 是一个 Valgrind 工具(为永远可爱的 memcheck 提供动力的框架),它分析缓存和分支命中/未命中。它会让您了解您在程序中实际获得了多少缓存命中/未命中。

于 2009-07-15T08:28:53.880 回答
18

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

缓存就是性能的一切;现在,CPU 比内存快得多,以至于您实际上几乎是针对内存总线而不是内核进行优化。

于 2009-07-15T08:27:51.247 回答
6

是的,看起来它主要是 L1 缓存未命中。

L1 缓存未命中的 10 个周期听起来确实很合理,可能有点偏低。

从 RAM 读取将需要 100 秒甚至可能是 1000 秒(我现在太累了,无法尝试做数学;))周期,所以它仍然是一个巨大的胜利。

于 2009-07-14T16:30:04.497 回答
3

如果您打算使用 cachegrind,请注意它只是一个缓存命中/未命中模拟器。它并不总是准确的。例如:如果您访问某个内存位置,例如 0x1234 循环 1000 次,cachegrind 将始终向您显示只有一次缓存未命中(第一次访问),即使您有以下内容:

clflush 0x1234 在你的循环中。

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

于 2011-11-14T23:18:15.447 回答
2

Lavalys Everest 运行的 3.4GHz P4 的一些数据:

  • L1 dcache 为 8K(cacheline 64 字节)
  • L2 为 512K
  • L1 获取延迟为 2 个周期
  • L2 获取延迟大约是您所看到的两倍:20 个周期

更多信息:http: //www.freeweb.hu/instlatx64/GenuineIntel0000F25_P4_Gallatin_MemLatX86.txt

(有关延迟,请查看页面底部)

于 2009-07-15T14:06:08.003 回答
0

如果没有更多的测试,很难确定任何事情,但根据我的经验,差异的规模肯定可以归因于 CPU L1 和/或 L2 缓存,尤其是在随机访问的情况下。通过确保每次访问至少与上一次访问至少有一些最小距离,您可能会使情况变得更糟。

于 2009-07-14T16:31:15.213 回答
-3

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

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

请注意,您在此处看到的内容肯定与缓存未命中(无论是 L1 还是 L1 和 L2)有关,因为通常,一旦您访问该缓存行上需要的任何内容,缓存就会提取同一缓存行上的数据更少的 RAM 行程。

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

于 2009-07-15T08:16:17.853 回答