编辑:出于参考目的(如果有人偶然发现这个问题),Igor Ostrovsky写了一篇关于缓存未命中的great post。它讨论了几个不同的问题并显示了示例数字。 结束修改
我做了一些测试<long story goes here>
,我想知道性能差异是否是由于内存缓存未命中造成的。以下代码演示了该问题,并将其归结为关键时序部分。以下代码有几个循环,以随机顺序访问内存,然后按升序地址顺序访问内存。
我在XP机器(使用VS2005:cl / O2编译)和Linux机器(gcc -Os)上运行它。两者产生了相似的时间这些时间以毫秒为单位。我相信所有循环都在运行并且未被优化(否则它将“立即”运行)。
*** Testing 20000 nodes Total Ordered Time: 888.822899 Total Random Time: 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 );
}
答案 0 :(得分:57)
这是尝试通过与烘焙巧克力饼干类比来洞察缓存未命中的相对成本......
你的手是你的注册。你需要1秒才能将巧克力碎片放入面团中。
厨房柜台是你的L1缓存,比寄存器慢十二倍。走到柜台需要12 x 1 = 12秒,拿起一袋核桃,然后清空一些手。
冰箱是你的二级缓存,比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年英特尔处理器,以下情况属实:
Gallery of Processor Cache Effects也很好地阅读了这个主题。
答案 1 :(得分:23)
虽然我无法回答这些数字是否有意义(我不熟悉缓存延迟,但是对于记录~10个周期的L1缓存未命中的声音),我可以为您提供Cachegrind作为一种工具,可帮助您实际查看两次测试之间缓存性能的差异。
Cachegrind是一个Valgrind工具(为永远可爱的memcheck提供支持的框架),用于配置缓存和分支命中/未命中。它会让您了解您在程序中实际获得的缓存命中/未命中数。
答案 2 :(得分:17)
缓存是性能的一切;现在CPU比内存快得多,你实际上几乎都在优化内存总线而不是核心。
答案 3 :(得分:6)
好吧,看起来它看起来主要是L1缓存未命中。
L1高速缓存未命中的10个周期听起来很合理,可能有点偏低。
从RAM中读取大约需要100秒或者甚至可能是1000秒(我现在太累了,不能尝试数学;))循环因此它仍然是一个巨大的胜利。
答案 4 :(得分:3)
如果您打算使用cachegrind,请注意它只是一个缓存命中/未命中模拟器。它并不总是准确的。例如:如果你访问一些内存位置,比如1000循环中的0x1234,那么即使你有类似的东西,cachegrind也总会告诉你只有一次缓存未命中(第一次访问):
循环中clflush 0x1234。
在x86上,这将导致所有1000次缓存未命中。
答案 5 :(得分:2)
来自Lavalys Everest的3.4GHz P4的一些数字:
更多信息: http://www.freeweb.hu/instlatx64/GenuineIntel0000F25_P4_Gallatin_MemLatX86.txt
(对于延迟,请查看页面底部)
答案 6 :(得分:0)
如果没有更多的测试,很难说肯定,但根据我的经验,差异的大小肯定可以归因于CPU L1和/或L2缓存,特别是在随机访问的情况下。通过确保每次访问至少与最后一次访问的距离最小,你可能会使情况变得更糟。
答案 7 :(得分:-2)
最简单的方法是拍摄目标cpu的缩放照片,并物理测量核心和1级缓存之间的距离。将该距离乘以电子每秒可以在铜中传播的距离。然后计算出你可以在同一时间内拥有多少个时钟周期。这是你在L1缓存未命中时浪费的最小cpu周期数。
您还可以计算以相同方式浪费的CPU周期数从RAM中获取数据的最低成本。你可能会感到惊讶。
请注意,您在此处看到的内容肯定与缓存未命中(无论是L1还是L1和L2)有关,因为通常缓存会在访问缓存上的任何内容时将数据提取到同一缓存行中-line需要较少的RAM访问。
但是,您可能也会看到RAM(即使它称为随机存取存储器)仍然更喜欢线性存储器访问。