我知道有关此主题的多个问题,但是,我没有看到任何明确的答案或基准测试。因此,我创建了一个简单的程序,该程序可以使用两个整数数组。第一个a
数组很大(64 MB),第二个b
数组很小,可以放入L1缓存中。该程序在a
上进行迭代,并以模块化的方式将其元素添加到b
的相应元素中(当到达b
的末尾时,程序将从头开始。)对于不同大小的b
,L1高速缓存未命中的测量数量如下:
测量是在具有32 kiB L1数据高速缓存的Xeon E5 2680v3 Haswell型CPU上进行的。因此,在所有情况下,b
都装入L1缓存中。但是,未命中的数量大大增加了b
内存足迹约16 kiB。这是可以预期的,因为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中),在实践中通常会遇到类似的情况。例如,在稀疏迭代线性求解器/本征求解器中,矩阵数据通常非常大(大于高速缓存容量),但是 vector 有时足够小以适合L3甚至L2高速缓存。然后,我们希望不惜一切代价将它们保留在那里。不幸的是,即使在每次求解器迭代中,矩阵数据的加载都可能导致特别是x vector 高速缓存行的失效,即使矩阵元素仅使用一次,也没有理由在处理它们之后将其保留在高速缓存中。
更新
我只是在Intel Xeon Phi KNC上进行了类似的实验,同时测量运行时间而不是L1丢失(我还没有找到一种方法来可靠地测量它们; PAPI和VTune提供了奇怪的指标。)结果在这里:
橙色曲线表示普通负载,具有预期的形状。蓝色曲线表示在指令前缀中设置了所谓的逐出提示(EH)的负载,灰色曲线表示手动逐出
a
的每个缓存行的情况; KNC启用的这两种技巧显然都可以奏效,就像我们想要的16 kb以上b
一样。被测循环的代码如下: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上,为
icpc
的常负荷变量(橙色曲线)预取生成的a_ptr
: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
,我也无法强制编译器为我生成非临时prefetchnig: 最佳答案
要专门回答标题问题:
是的,最近的1个主流Intel CPU支持正常2内存上的非临时加载-但是只能通过非临时预取指令“间接”进行,而不是直接使用movntdqa
这样的非临时加载指令。这与非临时存储相反,在非临时存储中,您可以直接使用相应的非临时存储说明3。
基本思想是,在任何常规加载之前向缓存行发出prefetchnta
,然后照常进行加载。如果该行尚未在缓存中,它将以非临时方式加载。非时态方式的确切含义取决于体系结构,但通常的模式是将行装载到至少L1或更高的缓存级别中。实际上,要使预取具有任何用途,就需要使该行至少加载到某个缓存级别中,以供以后的加载使用。还可以在高速缓存中对行进行特殊处理,例如,将行标记为驱逐的高优先级或限制其放置方式。
所有这一切的结果是,尽管从某种意义上说支持非临时性装载,但它们实际上只是部分非临时性装载,与存储区不同,在存储区中,您在任何高速缓存级别中都不会留下任何痕迹。非临时负载将导致某些缓存污染,但通常少于常规负载。确切的细节是特定于体系结构的,我在下面为现代英特尔提供了一些细节(您可以找到稍长的写入in this answer)。
Skylake客户
根据in this answer测试,似乎prefetchnta
Skylake的行为是正常地提取到L1缓存中,完全跳过L2,并以有限的方式提取到L3缓存中(可能只提取1或2种方式,因此总数量可用于nta
预取的L3的数量有限)。
这已经在Skylake client上进行了测试,但是我相信这种基本行为可能会向后扩展到Sandy Bridge和更早的版本(基于英特尔优化指南中的措辞),并且还会转发到Kaby Lake和基于Skylake客户端的更高版本的体系结构。因此,除非您使用的是Skylake-SP或Skylake-X部件或非常老的CPU,否则这可能是prefetchnta
可以预期的行为。
Skylake服务器
已知最新的唯一具有不同行为的英特尔芯片是Skylake server(用于Skylake-X,Skylake-SP和其他几行)。这大大改变了L2和L3体系结构,并且L3不再包含更大的L2。对于此芯片,prefetchnta
似乎同时跳过了L2和L3高速缓存,因此在此体系结构上,高速缓存污染仅限于L1。
此行为是reported by user Mysticial in a comment。那些评论中指出的不利之处在于,这会使prefetchnta
变得更加脆弱:如果您获得预取距离或计时错误(特别是在涉及到超线程且同级内核处于 Activity 状态时,这特别容易),并且数据在此之前已从L1撤出使用时,将一直返回主内存,而不是早期体系结构上的L3。
1这里的“最新”可能意味着过去十年左右的时间,但是我并不是要暗示较早的硬件不支持非临时预取:支持可能回到了prefetchnta
的引入,但我不支持具有检查的硬件,并且找不到有关它的现有可靠信息源。
2“正常”在这里仅表示WB(写回)内存,这是绝大多数时候在应用程序级别处理的内存。
3具体来说,NT存储指令是用于通用寄存器的movnti
和用于SIMD寄存器的movntd*
和movntp*
系列。