【发布时间】:2018-05-24 19:08:43
【问题描述】:
最近我一直在观察无法解释的内存密集型工作负载的性能影响。为了弄清楚这一点,我开始运行几个微基准测试以确定常见的性能参数,如缓存行大小和 L1/L2/L3 缓存大小(我已经知道它们,我只是想看看我的测量值是否反映了实际值)。
对于缓存行测试,我的代码大致如下(Linux C,但概念与 Windows 等类似):
char *array = malloc (ARRAY_SIZE);
int count = ARRAY_SIZE / STEP;
clock_gettime(CLOCK_REALTIME, &start_time);
for (int i = 0; i < ARRAY_SIZE; i += STEP) {
array[i]++;
}
clock_gettime(CLOCK_REALTIME, &end_time);
// calculate time per element here:
[..]
STEP 从 1 到 128 的变化表明,从 STEP=64 开始,我看到每个元素的时间没有进一步增加,即每次迭代都需要获取一个新的缓存行来支配运行时。
将ARRAY_SIZE 从 1K 变为 16384K,保持STEP=64 我能够创建一个漂亮的图,展示大致对应于 L1、L2 和 L3 延迟的步进模式。但是,为了获得可靠的数字,必须多次重复 for 循环,对于非常小的数组大小甚至 100,000 次。然后,在我的 IvyBridge 笔记本上,我可以清楚地看到 L1 以 64K 结束,L2 以 256K 结束,甚至 L3 以 6M 结束。
现在我的真正问题是:在 NUMA 系统中,任何单个内核都将获得远程主内存,甚至共享缓存,它不一定像其本地缓存和内存那样接近。我希望看到延迟/性能的差异,从而确定我可以分配多少内存,同时留在我的快速缓存/部分内存中。
为此,我改进了我的测试,以 1/10 MB 块的形式遍历内存,分别测量延迟,然后收集最快的块,大致如下:
for (int chunk_start = 0; chunk_start < ARRAY_SIZE; chunk_start += CHUNK_SIZE) {
int chunk_end = MIN (ARRAY_SIZE, chunk_start + CHUNK_SIZE);
int chunk_els = CHUNK_SIZE / STEP;
for (int i = chunk_start; i < chunk_end; i+= STEP) {
array[i]++;
}
// calculate time per element
[..]
一旦我开始将ARRAY_SIZE 增加到大于 L3 大小的值,我就会得到非常不可靠的数字,即使是大量重复也无法平衡。我无法确定可用于性能评估的模式,更不用说确定 NUMA 条带的确切开始、结束或位置了。
然后,我认为 硬件预取器 足够聪明,可以识别我的简单访问模式,并在我访问它们之前简单地将所需的行提取到缓存中。向数组索引添加一个随机数会增加每个元素的时间,但似乎没有太大帮助,可能是因为我每次迭代都有一个rand () 调用。预先计算一些随机值并将它们存储在数组中对我来说似乎不是一个好主意,因为这个数组也会存储在热缓存中并扭曲我的测量值。将STEP 增加到 4097 或 8193 也无济于事,预取器一定比我聪明。
我的方法是否明智/可行,还是我错过了大局?是否有可能观察到这样的 NUMA 延迟?如果是,我做错了什么? 我禁用地址空间随机化只是为了确定并排除奇怪的缓存别名效应。在测量之前是否需要对操作系统方面的其他方面进行调整?
【问题讨论】:
-
Re:预取器,英特尔预取器执行黑魔法。我听说它应用多个多项式拟合来神奇地以您当前的步幅预取,等等。您最好的选择可能是使用 MTRR 来标记不可缓存的内存范围,然后在该范围内进行基准测试。跨度>
标签: c performance caching prefetch numa