3

这是 cachegrind 输出的一部分。这部分代码已经执行了 1224 次。elmg1 是一个大小为 16 x 20 的无符号长数组。我的机器 L1 缓存大小为 32KB,缓存行大小为 64B,8 路集关联。

  1. 对于 (i = 0; i < 20; i++) 78,336 2,448 2 50,184 0 0 1,224 0 0
  2. {
  3. telm01 = elmg1[i]; 146,880 0 0 73,440 0 0 24,480 0 0
  4. telm31 = (telm01 << 3) ^ val1; 97,920 0 0 48,960 0 0 24,480 0 0
  5. telm21 = (telm01 << 2) ^ (val1 >> 1); 146,880 1,224 1 48,960 0 0 24,480 0 0
  6. telm11 = (telm01 << 1) ^ (val1 >> 2); 146,880 0 0 48,960 0 0 24,480 0 0
  7. }

A. 我把它放在这里的原因是,在 for 循环内的第三行,我看到了许多 I1 未命中(还有一个 L2 未命中)。这有点令人困惑,我猜不出原因?

B. 我正在尝试优化(时间)一部分代码。以上只是一个小片段。我认为在我的程序内存访问中花费了我很多。就像上面的例子一样,elmg1 是一个 16 x 20 大小的无符号长数组。当我尝试在代码中使用它时,总会有一些失误,而在我的程序中,这些变量经常出现。有什么建议么?

C. 我需要分配和(有时初始化)这些无符号长整数。你能建议我更喜欢哪一个,calloc 或数组声明,然后显式初始化。顺便说一句,缓存处理它们的方式会有什么不同吗?

谢谢。

4

1 回答 1

3

您是否尝试过展开循环?

  1. 我现在不会担心 L1 未命中。1224 次中的一次 L2 未命中也是可以的,cpu 必须在某个时候将值加载到缓存中。
  2. 与程序的其余部分相比,此代码的 L2 未命中百分比是多少?
  3. 使用 calloc(),如果数组大小始终相同并且您使用常量作为大小,那么编译器可以优化数组的归零。此外,唯一会影响缓存行使用的是对齐,而不是它是如何初始化的。

编辑:很难以这种方式阅读并第一次读错的数字。

让我们确保我正在阅读第 5 行的正确数字:

Ir    146,880
I1mr  1,224
ILmr  1
Dr    48,960
D1mr  0
DLmr  0
Dw    24,480
D1mw  0
DLmw  0

L1 高速缓存分为两个 32KByte 高速缓存,一个用于代码 I1,一个用于数据 D1。IL & DL 是 L2 或 L3 缓存,由数据和指令共享。

大量的 I1mr 是指令未命中而不是数据未命中,这意味着循环代码正在从 I1 指令缓存中弹出。

第 1 行和第 5 行的 I1 未命中总数为 3672,即 1224 的 3 倍,因此每次运行循环时,您都会获得 3 个 I1 缓存未命中,其中 64 字节缓存行,这意味着您循环代码大小在 128-192 字节之间以覆盖 3 个缓存行。所以第 5 行的那些 I1 未命中是因为那是循环代码穿过最后一个缓存行的地方。

我建议使用 KCachegrind 查看 cachegrind 的结果

编辑:有关缓存行的更多信息。

那个循环代码看起来不像它本身被调用了 1224 次,所以这意味着有更多的代码将这个代码推出 I1 缓存。

您的 32Kbyte I1 缓存分为 512 条缓存线(每条 64 字节)。“8-way set associative”部分意味着每个内存地址仅映射到这 512 个高速缓存行中的 8 个。如果您所配置的整个程序是一个连续的 32 KB 内存块,那么它将全部放入 I1 缓存中,并且不会弹出任何内容。很可能不是这种情况,对于相同的 8 个高速缓存行,将有超过 8 个 64 字节的代码块。假设你的整个程序有 1Mbyte 的代码(这包括库),那么每组 8 个缓存行将有大约 32 条(1Mbyte/32Kbyte)的代码内容用于相同的 8 个缓存行。

阅读这篇 lwn.net 文章,了解有关 CPU 缓存的所有血腥细节

编译器不能总是检测程序的哪些函数将是热点(多次调用),哪些将是代码点(即错误处理程序代码,几乎从不运行)。GCC 有函数属性hot/cold,它允许你将函数标记为 hot/cold,这将允许编译器将 hot 函数组合在一个内存块中以获得更好的缓存使用(即冷代码不会推出 hotcode的缓存)。

无论如何,那些 I1 失误真的不值得花时间担心。

于 2010-11-01T07:43:59.103 回答