Section 3.3.2描述了一个简单的基准,它演示了缓存对顺序读取的影响。该基准测试包括遵循一个(顺序排列的)循环链表,并测量每个节点花费的平均时间。节点结构如下:
struct l {
struct l *n;
long int pad[NPAD];
};
大小为N
的工作集包含N/sizeof(l)
个节点。有了足够的填充,每个指针将存储在不同的缓存行中,因此我们需要一个大小至少为64 * N/sizeof(l)
的缓存来跟踪该周期,而无需进行昂贵的内存访问。
这是令我感到困惑的部分:假设64 * N/sizeof(l)
恰好是最大缓存的大小。例如,如果我们通过更改填充将sizeof(l)
减半,则N
还需要小2倍,以便我们将所有内容都保留在缓存中。因此,以不同的步伐,我们应该以不同的工作集大小耗尽缓存。但是文字中的Figure 3.11(我没有足够的代表来嵌入图像)似乎与此矛盾-三种NPAD尺寸的访问时间突然跳到N=2^19
。
我想念什么?
起初我以为填充也可能会被缓存-但这不是对this question的答案所解释的那样。
答案 0 :(得分:1)
通过足够的填充,每个指针都存储在不同的缓存行中,因此我们需要一个大小至少为
64 * N/sizeof(l)
的缓存来跟踪该周期,而无需进行昂贵的内存访问。
否,如果缓存行为64字节,并且sizeof(struct l)
至少为64,则每个节点都位于不同的缓存行中。但这不是3.3.2节的重点。
假设
64 * N/sizeof(l)
恰好是最大缓存的大小。例如,如果我们通过更改填充将sizeof(l)
减半,那么如果我们希望所有内容都适合缓存,则N也需要小2倍。
缓存的大小将是固定的字节数,而不是诸如64 * N/sizeof(l)
之类的计算。在本小节的部分中,Drepper使用1 MiB(2 20 字节)L2高速缓存。此外,Drepper在将“工作集大小”(显然是总数组大小,尽管在某些试验中并非所有数组都可以访问)描述为2 N 个字节的情况下使用N,因此“工作集”如果2 N 个字节小于或等于缓存大小,则适合缓存。如果数组大小为2 N 个字节,则元素数必须为2 N / sizeof(struct l)
。
在图3.11中,您看到的是:
我对Drepper的某些说法表示怀疑。例如,关于15和31 long int
的填充之间的差异(显然将元素大小从128字节更改为256字节),他写道:
元素大小妨碍预取工作的原因是硬件预取的限制:它不能跨越页面边界。
但是,页面大小为4096字节(从其他地方提到的60个页面为245,760字节),元素间距为128到256字节,读取将分别仅遇到32和16个元素的页面边界。每个元素从大约145个周期跳到大约320个周期,因为每个元素还有一个额外的1/32页面转换(每16个元素1个减每32个元素1个=每个元素1/32)意味着1/32•C = 320− 145,其中C是页面转换所花费的周期数。那么C =(320-145)•32 = 5,600。如果没有更多针对性的测试或支持文档,我将不会相信这么大的数字。
Drepper还用括号斜体写:
是的,这有点不一致,因为在其他测试中,我们在元素大小中计算了结构的未使用部分,并且可以定义NPAD,以便每个元素填充一页。在这种情况下,工作集的大小将非常不同。不过,这并不是该测试的重点,而且由于预取仍然无效,因此差异不大。
这与他先前对工作集的描述不符,他说工作集2 N 个字节包含2 N / sizeof(struct l)
个元素。后者显示他的“工作集”是整个数组,即使仅访问每个元素的一部分也是如此。前者说,工作集将根据每个元素的访问量而有所不同。
总的来说,我认为Drepper在描述和原因方面有些松懈。特别是在没有示例代码的情况下,很难遵循某些测试描述或复制它们。假定的各种大小的数组元素建模实际上是跳过内存访问仅访问部分的建模-如果那些是程序正在使用的实际数组元素,它将加载整个元素,并且预取将很好地使用每个字节。一旦理解了这种令人困惑的描述,这不会使Drepper的测量无效,但这是一种误导性描述。同样,在不同地方对“工作集”的差异描述也令人困惑。
因此,我的建议是花一点儿心思,不要期望它成为缓存和内存性能的完整而清晰的规范。