【问题标题】:What is the Cost of an L1 Cache Miss?L1 缓存未命中的成本是多少?
【发布时间】:2010-11-10 17:21:46
【问题描述】:

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

我做了一些测试<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 );

}

【问题讨论】:

  • 他的问题是:“这些数字有意义吗?”
  • 抱歉 - 我把这个问题埋在了太多的文字中。但是,是的,问题是这些数字是否有意义。 L1 缓存未命中的 10 个周期是否正确?
  • 您应该阅读 Ulrich Drepper 的 "What every programmer should know about memory" - 它深入探讨了内存访问的时间、访问模式和缓存交互。
  • 问题中链接到的 Igor Ostrovsky 非常好。 +1 只是为了指导我。

标签: c caching memory-access


【解决方案1】:

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

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

厨房柜台是您的 L1 缓存,比寄存器慢 12 倍。 需要 12 x 1 = 12 秒才能走到柜台,拿起一袋核桃,然后将一些倒入您的手。

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

橱柜是你的 L3 缓存,比 L2 慢三倍。 需要 3 x 48 = 2 分 24 秒才能走到橱柜,弯腰,开门,root到处找烘焙用品罐,从橱柜里拿出来,打开,挖找到发酵粉,把它放在柜台上,把你洒在地板上的烂摊子扫干净。

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

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

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

数字取自 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 寄存器)
  • DRAM 延迟 = 65 ns = 3 GHz CPU 上的 195 个周期(5 x L3、15 x L2、60 x L1、720 x 寄存器)

Gallery of Processor Cache Effects 也很好地阅读了这个主题。

【讨论】:

  • O(1) 中的 1 总是拖累。很好的答案,应该被接受!
  • 很好的答案!此外,这可以扩展到共享相同橱柜(L3 缓存)的多个小厨房(核心);如果一个厨师去商店买更多面粉,其他人都可以从那里拿走。
  • 我还要补充一点:在虚拟内存的情况下,访问交换页面(即需要从磁盘读取数据的页面)就像发现商店缺货一样肉桂粉,他们需要从中国订购一批新产品 - 运输期为 6 周。
【解决方案2】:

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

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

【讨论】:

  • 非常好。感谢您的指针。我已经知道 Valgrind 但以前没有使用过它(我的大部分开发都是在 Win32 上)。我刚刚在 Linux 机器上运行它,它报告测试的“随机”部分有 41% 的未命中率。测试的“有序”部分的失误率可以忽略不计。这两个部分都没有任何 L2 未命中率可言。
【解决方案3】:

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

clflush 0x1234 在你的循环中。

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

【讨论】:

  • 您能否解释一下为什么在 x86 上会发生 1000 次缓存未命中
  • 如果这是真的,cachegrind 不能简单地在他们的缓存模拟中添加对clflush 指令的支持吗?
【解决方案4】:

来自 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

(延迟查看页面底部)

【讨论】:

    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

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

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

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

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

      【讨论】:

      • 电子的速度与电流/电压的速度无关。电子移动非常缓慢。
      • 是的,更多的是与电容有关,以及振铃需要多长时间才能稳定下来。
      • @Skizz,你能告诉我如何将这些单位转换为秒,以便我可以将其作为答案吗?
      • 您至少可以将铜中的电波速度包括在内,即 IIRC 约为 0.6c(对于此目的足够接近)
      • 如果缓存访问是无时钟异步电路,这将是有意义的。真正的处理器是流水线的,变化只发生在时钟边沿,加载/存储流水线具有确定性操作的流水线寄存器。物理距离仅与设计硅的工程师有关。延迟的一个原因是物理距离,当然,但您无法从照片中确定延迟。
      【解决方案7】:

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

      【讨论】:

        【解决方案8】:

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

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

        从 RAM 读取大约需要 100 秒甚至可能是 1000 秒(我太累了,现在无法尝试进行数学计算;))周期,因此它仍然是一个巨大的胜利。

        【讨论】:

        • “有点偏低” - 有 80K 的数据和 32K 的 L1,如果每次 fetch 都错过缓存,你会感到失望,所以有点低对我来说是有意义的。
        • 好点……而且顺序已经随机化的事实意味着必须有大约 50/50 的缓存未命中才能命中。当然,想出一个读取模式会很好也很容易,这意味着每次访问都会丢失:)
        • 我同意 - 好点。如果缓存是 32K 并且主要用于保存数组,那么可能有 40% 的引用会被命中。因此,60% 的未命中率将使每次未命中的成本高达约 17 个周期(再次假设我的数学是正确的)。
        • sandpile.org/impl/p4.htm 表明从 90 到 65nm P4 读取 L2 缓存的延迟在 18 到 20 个周期之间。所以上面马克的快速计算看起来很不错:)
        • 事实上,假设每次未命中 18 个周期并将其插入,我们得到的值约为 56.3% 的 L1 缓存未命中,假设 20 个周期给我们的值为 50.6% 的 L1 缓存未命中。
        猜你喜欢
        • 2016-09-03
        • 2020-05-19
        • 1970-01-01
        • 2012-05-03
        • 2019-11-23
        • 1970-01-01
        • 2017-09-26
        • 1970-01-01
        相关资源
        最近更新 更多