我知道关于这个主题的多个问题,但是,我没有看到任何明确的答案或任何基准测量。因此,我创建了一个简单的程序,它使用两个整数数组。第一个数组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
的每个高速缓存行的情况; KNC启用的这些技巧显然可以按照我们想要的b
超过16 kiB。测量循环的代码如下:
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 :(
答案 0 :(得分:4)
具体回答标题问题:
是,最近的 1 主流Intel CPU支持普通 2 内存的非临时负载 - 但仅限&#34;间接&#34;通过非时间预取指令,而不是直接使用像movntdqa
这样的非时态加载指令。这与非临时存储形成对比,在非存储存储中,您可以直接使用相应的非临时存储指令 3 。
基本思想是在正常加载之前向缓存行发出prefetchnta
,然后正常发出加载。如果该行尚未在缓存中,则将以非时间方式加载。 非时间时尚的确切含义取决于体系结构,但一般模式是该行至少加载到L1并且可能加载到某些更高的缓存级别。实际上,对于任何使用的预取,它需要使该行至少加载到某些高速缓存级别以供稍后的加载消耗。该行也可以在缓存中专门处理,例如通过将其标记为驱逐的高优先级或限制它的放置方式。
所有这一切的结果是,虽然从某种意义上说,非临时负载是支持,但它们实际上只是部分非暂时性的,而不像你在任何一个地方都没有留下任何线条痕迹的商店缓存级别。非临时负载将导致某些缓存污染,但通常低于常规负载。确切的细节是特定于体系结构的,我在下面列出了现代英特尔的一些细节(您可以找到稍长的写入in this answer)。
基于测试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 server(用于Skylake-X,Skylake-SP和其他一些行)。这有一个相当大的L2和L3架构,L3不再包含更大的L2。对于这个芯片,似乎prefetchnta
跳过两个 L2和L3缓存,因此在这个架构上缓存污染仅限于L1。
此行为为reported by user Mysticial in a comment。正如这些评论中所指出的那样,缺点是这会使prefetchnta
更加脆弱:如果你得到预取距离或时间错误(特别是当涉及超线程并且兄弟核心处于活动状态时很容易),并且数据得到了在使用之前从L1中驱逐出去,你将一直回到主存储器而不是早期架构上的L3。
1 最近这里可能意味着过去十年左右的任何事情,但我并不意味着暗示早期的硬件并不支持非-temporal prefetch:支持可能会直接回到prefetchnta
的介绍,但我没有硬件来检查它,并且无法找到现有的可靠信息来源在它上面。
2 正常这里只是指WB(写回)内存,它是绝大部分时间在应用程序级别处理的内存。
3 具体来说,NT存储指令对于通用寄存器是movnti
,对于SIMD寄存器是movntd*
和movntp*
族。
答案 1 :(得分:2)
我回答了自己的问题,因为我从英特尔信息技术峰会上发现了以下帖子,这对我来说很有意义。它由John McCalpin撰写:
主流处理器的结果并不令人惊讶 - 在没有真正的“暂存器”记忆的情况下,不清楚是否有可能设计出一种不会出现令人讨厌的意外的“非暂时”行为的实现。 过去使用的两种方法是(1)加载缓存行,但将其标记为LRU而不是MRU,以及(2)将缓存行加载到组关联缓存的一个特定“集合”中。在任何一种情况下,都可以相对容易地生成缓存在处理器完成读取之前丢弃数据的情况。
这两种方法都会在运行超过少量数组的情况下降低性能,并且在考虑超线程时没有“陷阱”的情况下实现起来要困难得多。
在其他情况下,我主张实现“加载多个”指令,这些指令可以保证缓存行的全部内容将以原子方式复制到寄存器中。我的理由是硬件绝对保证高速缓存行以原子方式移动,并且将高速缓存行的其余部分复制到寄存器所需的时间非常小(额外1-3个周期,具体取决于处理器生成)安全地实现为原子操作。
从Haswell开始,核心可以在一个周期内读取64个字节(2个256位对齐的AVX读数),因此暴露于非预期的副作用会变得更低。
从KNL开始,全高速缓存行(对齐)加载应该是“自然”原子的,因为从L1数据高速缓存到核心的传输是完整的高速缓存行,并且所有数据都被放入目标AVX- 512寄存器。 (这并不意味着英特尔在实施过程中保证原子性!我们无法了解设计师必须考虑的可怕角落情况,但是大部分时间的结论是合理的对齐的512位负载将以原子方式发生。)使用这种“自然”64字节原子性,过去用于减少由于“非暂时”负载引起的高速缓存污染的一些技巧可能值得另外看看....
MOVNTDQA指令主要用于从映射为“写入组合”(WC)的地址范围读取,而不是从映射为“回写”(WB)的正常系统存储器读取。 SWDM第2卷中的描述表明,对于WB区域,实现“可能”对MOVNTDQA做了一些特殊处理,但重点是WC存储器类型的行为。
“写入组合”存储器类型几乎从不用于“实际”存储器 - 它几乎专门用于存储器映射的IO区域。
请在此处查看整篇文章:https://software.intel.com/en-us/forums/intel-isa-extensions/topic/597075