C程序,用于确定缓存的级别和大小

完全重写/更新为了清晰(和你的理智,它的长期太长)… ( 旧post )

对于赋值,我需要找到每个缓存的级别(L1,L2,…)和大小。 给出提示和我到目前为止发现的内容:我认为这个想法是创建不同大小的数组并阅读它们。 定时这些操作:

sizes = [1k, 4k, 256K, ...] foreach size in sizes create array of `size` start timer for i = 0 to n // just keep accessing array arr[(i * 16) % arr.length]++ // i * 16 supposed to modify every cache line ... see link record/print time 

更新(9月28日下午6:57 UTC + 8)

另见完整来源

好了,现在按照@ mah的建议,我可能已经修复了SNR比率问题……还找到了一种计算代码的方法(来自实验室示例代码的wall_clock_time

但是,我似乎得到了不正确的结果:我在Intel Core i3 2100上:[ SPECS ]

  • L1:2 x 32K
  • L2:2 x 256K
  • L3:3MB

我得到的结果,在图表中:

lengthMod:1KB到512K

在此处输入图像描述

第一峰的基数是32K ……合理……第二个是384K ……为什么? 我期待256?

lengthMod:512k到4MB

在此处输入图像描述

那为什么这个范围会变得一团糟?


我还阅读了有关其他应用程序的预取或干扰的内容,因此我在脚本运行时关闭了尽可能多的内容,它始终如一(通过多次运行)1MB及以上的数据总是如此混乱?

测量时间所需的时间(即调用clock()函数的时间)比执行arr[(i*16)&lengthMod]++所需的时间长许多(许多……)倍。 arr[(i*16)&lengthMod]++ 。 这种极低的信噪比(以及其他可能的陷阱)使您的计划无法工作。 问题的很大一部分是你试图测量循环的单个迭代; 您链接的示例代码正在尝试测量一组完整的迭代(在开​​始循环之前读取时钟;在从循环中出现后再次读取它;不要在循环内使用printf())。

如果您的循环足够大,您可能能够克服信噪比问题。

至于“什么元素正在增加”; arr是1MB缓冲区的地址; arr[(i * 16) & lengthMod]++; 导致(i * 16) * lengthMod从该地址生成偏移量; 该偏移量是增加的int的地址。 您正在执行一个class次(i * 16将变为i << 4),一个逻辑和一个加法,然后是读取/添加/写入或单个增量,具体取决于您的CPU)。

编辑:如上所述,由于内存访问的相对速度(缓存或无缓存)和调用函数来测量时间,您的代码会受到较差的SNR(信噪比)的影响。 为了获得您当前获得的时间,我假设您修改了代码,如下所示:

 int main() { int steps = 64 * 1024 * 1024; int arr[1024 * 1024]; int lengthMod = (1024 * 1024) - 1; int i; double timeTaken; clock_t start; start = clock(); for (i = 0; i < steps; i++) { arr[(i * 16) & lengthMod]++; } timeTaken = (double)(clock() - start)/CLOCKS_PER_SEC; printf("Time for %d: %.12f \n", i, timeTaken); } 

这会将测量移到循环外部,因此您不会测量单次访问(这实际上是不可能的),而是您正在测量steps访问。

您可以根据需要自由增加steps ,这将对您的时间产生直接影响。 由于您接收的时间太近,并且在某些情况下甚至倒置(您的时间在大小之间振荡,这可能不是由缓存引起的),您可能会尝试将steps值更改为256 * 1024 * 1024甚至大。

注意:您可以创建适合已签名的int(应该足够大)的步骤,因为逻辑并确保您在缓冲区中包装。

在10分钟搜索英特尔指令手册和另外10分钟的编码后,我想出了这个(对于基于英特尔的处理器):

 void i386_cpuid_caches () { int i; for (i = 0; i < 32; i++) { // Variables to hold the contents of the 4 i386 legacy registers uint32_t eax, ebx, ecx, edx; eax = 4; // get cache info ecx = i; // cache id __asm__ ( "cpuid" // call i386 cpuid instruction : "+a" (eax) // contains the cpuid command code, 4 for cache query , "=b" (ebx) , "+c" (ecx) // contains the cache id , "=d" (edx) ); // generates output in 4 registers eax, ebx, ecx and edx // taken from http://download.intel.com/products/processor/manual/325462.pdf Vol. 2A 3-149 int cache_type = eax & 0x1F; if (cache_type == 0) // end of valid cache identifiers break; char * cache_type_string; switch (cache_type) { case 1: cache_type_string = "Data Cache"; break; case 2: cache_type_string = "Instruction Cache"; break; case 3: cache_type_string = "Unified Cache"; break; default: cache_type_string = "Unknown Type Cache"; break; } int cache_level = (eax >>= 5) & 0x7; int cache_is_self_initializing = (eax >>= 3) & 0x1; // does not need SW initialization int cache_is_fully_associative = (eax >>= 1) & 0x1; // taken from http://download.intel.com/products/processor/manual/325462.pdf 3-166 Vol. 2A // ebx contains 3 integers of 10, 10 and 12 bits respectively unsigned int cache_sets = ecx + 1; unsigned int cache_coherency_line_size = (ebx & 0xFFF) + 1; unsigned int cache_physical_line_partitions = ((ebx >>= 12) & 0x3FF) + 1; unsigned int cache_ways_of_associativity = ((ebx >>= 10) & 0x3FF) + 1; // Total cache size is the product size_t cache_total_size = cache_ways_of_associativity * cache_physical_line_partitions * cache_coherency_line_size * cache_sets; printf( "Cache ID %d:\n" "- Level: %d\n" "- Type: %s\n" "- Sets: %d\n" "- System Coherency Line Size: %d bytes\n" "- Physical Line partitions: %d\n" "- Ways of associativity: %d\n" "- Total Size: %zu bytes (%zu kb)\n" "- Is fully associative: %s\n" "- Is Self Initializing: %s\n" "\n" , i , cache_level , cache_type_string , cache_sets , cache_coherency_line_size , cache_physical_line_partitions , cache_ways_of_associativity , cache_total_size, cache_total_size >> 10 , cache_is_fully_associative ? "true" : "false" , cache_is_self_initializing ? "true" : "false" ); } } 

参考: http : //download.intel.com/products/processor/manual/325462.pdf 3-166 Vol。 2A

这比测量缓存延迟更加可靠,因为在现代处理器上关闭缓存预取几乎是不可能的。 如果您需要针对不同处理器架构的类似信息,则必须参阅相应的手册。

编辑:添加缓存类型描述符。 Edit2:添加了缓存级别指示器。 Edit3:添加了更多文档。

我知道这个! (实际上由于预取而非常复杂)

  for (times = 0; times < Max; time++) /* many times*/ for (i=0; i < ArraySize; i = i + Stride) dummy = A[i]; /* touch an item in the array */ 

更改步幅允许您测试缓存的属性。 通过查看图表,您将获得答案。

请看幻灯片35-42 http://www.it.uu.se/edu/course/homepage/avdark/ht11/slides/11_Memory_and_optimization-1.pdf

Erik Hagersten是一位非常优秀的老师(也非常称职,曾担任太阳的首席架构师),所以请看看其余的幻灯片,以获得更多精彩的解释!

要回答1MB以上奇怪数字的问题,这很简单; 与分支预测有关的高速缓存驱逐策略,以及在核之间共享L3高速缓存的事实。

核心i3有一个非常有趣的缓存结构。 实际上任何现代处理器都可以。 你应该在维基百科上阅读它们; 它有各种各样的方式来决定“好吧,我可能不需要这个…”然后它可以说“我会把它放在受害者缓存中”或任何数量的东西。 基于大量因素和单独的设计决策,L1-2-3高速缓存定时可能非常复杂。

最重要的是,所有这些决定以及更多内容(请参阅有关该主题的维基百科文章)必须在两个核心的缓存之间同步。 将共享L3缓存与单独的L1和L2缓存同步的方法可能很难看,它们可能涉及反向跟踪和重新执行计算或其他方法。 你不太可能拥有一个完全免费的第二核心,没有竞争L3缓存空间,从而导致同步怪异。

一般来说,如果您正在处理数据,比如卷积内核,您需要确保它适合L1缓存并围绕它调整算法。 L3缓存并不是真的意味着以你的方式处理数据集(虽然它比主内存更好!)

我发誓,如果我是那个必须实现缓存算法的人,我会疯了。

对于不同步幅的基准测试,你可以尝试lmbench包中的lat_mem_rd,它是开源的: http : //www.bitmover.com/lmbench/lat_mem_rd.8.html

我已将我的Windows端口发布到http://habrahabr.ru/post/111876/ – 在这里进行复制是非常漫长的。 那是两年前的事,我没有用现代CPU测试它。

对于Windows,您可以使用GetLogicalProcessorInformation方法。

对于linux,您可以使用sysconf() 。 您可以在/usr/include/unistd.h/usr/include/bits/confname.h找到sysconf有效参数。