【发布时间】:2017-02-27 01:22:47
【问题描述】:
我知道有关此主题的多个问题,但是,我没有看到任何明确的答案或任何基准测量。因此,我创建了一个使用两个整数数组的简单程序。第一个数组a 非常大(64 MB),第二个数组b 很小以适合L1 缓存。程序迭代a,并将其元素添加到模块意义上的b 的相应元素中(当到达b 的末尾时,程序又从头开始)。不同大小b的L1缓存未命中数实测如下:
测量是在具有 32 kiB L1 数据缓存的 Xeon E5 2680v3 Haswell 型 CPU 上进行的。因此,在所有情况下,b 都适合 L1 缓存。但是,未命中的数量大幅增加了大约 16 kiB b 内存占用。这可能是意料之中的,因为此时a 和b 的负载会导致缓存行从b 的开头开始失效。
绝对没有理由将a 的元素保留在缓存中,它们只使用一次。因此,我运行了一个带有a 数据的非临时负载的程序变体,但未命中的数量没有改变。我还运行了一个对a 数据进行非临时预取的变体,但结果仍然相同。
我的基准代码如下(显示了不带非临时预取的变体):
int main(int argc, char* argv[])
{
uint64_t* a;
const uint64_t a_bytes = 64 * 1024 * 1024;
const uint64_t a_count = a_bytes / sizeof(uint64_t);
posix_memalign((void**)(&a), 64, a_bytes);
uint64_t* b;
const uint64_t b_bytes = atol(argv[1]) * 1024;
const uint64_t b_count = b_bytes / sizeof(uint64_t);
posix_memalign((void**)(&b), 64, b_bytes);
__m256i ones = _mm256_set1_epi64x(1UL);
for (long i = 0; i < a_count; i += 4)
_mm256_stream_si256((__m256i*)(a + i), ones);
// load b into L1 cache
for (long i = 0; i < b_count; i++)
b[i] = 0;
int papi_events[1] = { PAPI_L1_DCM };
long long papi_values[1];
PAPI_start_counters(papi_events, 1);
uint64_t* a_ptr = a;
const uint64_t* a_ptr_end = a + a_count;
uint64_t* b_ptr = b;
const uint64_t* b_ptr_end = b + b_count;
while (a_ptr < a_ptr_end) {
#ifndef NTLOAD
__m256i aa = _mm256_load_si256((__m256i*)a_ptr);
#else
__m256i aa = _mm256_stream_load_si256((__m256i*)a_ptr);
#endif
__m256i bb = _mm256_load_si256((__m256i*)b_ptr);
bb = _mm256_add_epi64(aa, bb);
_mm256_store_si256((__m256i*)b_ptr, bb);
a_ptr += 4;
b_ptr += 4;
if (b_ptr >= b_ptr_end)
b_ptr = b;
}
PAPI_stop_counters(papi_values, 1);
std::cout << "L1 cache misses: " << papi_values[0] << std::endl;
free(a);
free(b);
}
我想知道的是 CPU 供应商是否支持或将支持非临时加载/预取或任何其他方式如何将某些数据标记为不在缓存中保留(例如,将它们标记为 LRU)。在某些情况下,例如在 HPC 中,类似的场景在实践中很常见。例如,在稀疏迭代线性求解器/特征求解器中,矩阵数据通常非常大(大于缓存容量),但向量有时小到足以放入 L3 甚至 L2 缓存。然后,我们希望不惜一切代价将它们留在那里。不幸的是,加载矩阵数据可能会导致特别是 x 向量缓存行无效,即使在每次求解器迭代中,矩阵元素仅使用一次,并且没有理由在处理完它们后将它们保留在缓存中。
更新
我刚刚在 Intel Xeon Phi KNC 上做了一个类似的实验,同时测量运行时而不是 L1 未命中(我还没有找到可靠测量它们的方法;PAPI 和 VTune 给出了奇怪的指标。)结果如下:
橙色曲线代表普通载荷,它具有预期的形状。蓝色曲线表示在指令前缀中设置了所谓的驱逐提示(EH)的负载,灰色曲线表示a的每个缓存行被手动驱逐的情况;对于超过 16 kiB 的b,KNC 启用的这两个技巧显然都像我们想要的那样工作。实测循环代码如下:
while (a_ptr < a_ptr_end) {
#ifdef NTLOAD
__m512i aa = _mm512_extload_epi64((__m512i*)a_ptr,
_MM_UPCONV_EPI64_NONE, _MM_BROADCAST64_NONE, _MM_HINT_NT);
#else
__m512i aa = _mm512_load_epi64((__m512i*)a_ptr);
#endif
__m512i bb = _mm512_load_epi64((__m512i*)b_ptr);
bb = _mm512_or_epi64(aa, bb);
_mm512_store_epi64((__m512i*)b_ptr, bb);
#ifdef EVICT
_mm_clevict(a_ptr, _MM_HINT_T0);
#endif
a_ptr += 8;
b_ptr += 8;
if (b_ptr >= b_ptr_end)
b_ptr = b;
}
更新 2
在 Xeon Phi 上,为 a_ptr 的正常负载变体(橙色曲线)预取生成 icpc:
400e93: 62 d1 78 08 18 4c 24 vprefetch0 [r12+0x80]
当我手动(通过十六进制编辑可执行文件)将其修改为:
400e93: 62 d1 78 08 18 44 24 vprefetchnta [r12+0x80]
我得到了想要的结果,甚至比蓝色/灰色曲线还要好。但是,我无法强制编译器为我生成非临时预取,即使在循环之前使用 #pragma prefetch a_ptr:_MM_HINT_NTA :(
【问题讨论】:
-
好东西。您能否发布或分享(例如在 GitHub 上)完整代码,包括带有预取功能的变体?
-
@BeeOnRope:见github.com/DanielLangr/ntload
-
太棒了。将您的问题表述为一个问题可能是值得的。就目前而言,这只是研究,但你想知道什么问题?如果我理解正确,您想知道以下内容:“当前的 x86 架构是否支持非临时负载?”。我认为您可以省略预取部分,因为它确实包含在“加载”中 - load 数据的方法确实是为了确保它被预取。
-
因为我在任何地方都看不到这个链接:这个微基准的想法来自:software.intel.com/en-us/forums/intel-isa-extensions/topic/…
-
这很难,因为 SKL 在只运行内存绑定代码时决定自行降频,但这会影响内存带宽。
标签: c++ c caching x86 prefetch