代码之家  ›  专栏  ›  技术社区  ›  Mark Wilkins

一级缓存丢失的成本是多少?

  •  68
  • Mark Wilkins  · 技术社区  · 15 年前

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

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

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

    *** 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)。

    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 );
    
    }
    
    8 回复  |  直到 6 年前
        1
  •  23
  •   Prof. Falken    13 年前

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

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

        2
  •  56
  •   yoyo    6 年前

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

    你的手是你的寄存器。 你花了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 Cache Flushing Fallacy 博客文章表明,对于特定的2012年英特尔处理器,以下是正确的:

    • 寄存器访问=每周期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寄存器)

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

    Mmmm, cookies ...

        3
  •  17
  •   Crashworks    15 年前

    对于一级缓存未命中,3.2ns是完全合理的。相比之下,在一个特定的现代多核PowerPC CPU上,一级未命中是关于 四十 周期——一些内核比其他内核长一点,这取决于它们与二级缓存的距离(是的,真的)。二级未命中是 至少 六百 周期。

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

        4
  •  6
  •   Goz    15 年前

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

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

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

        5
  •  3
  •   user1046529    13 年前

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

    循环中的clflush 0x1234。

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

        6
  •  2
  •   terminus    15 年前

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

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

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

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

        7
  •  0
  •   Tim Sylvester    15 年前

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

        8
  •  -2
  •   Jasper Bekkers    15 年前

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

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

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

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