比方说,我正在遍历10个不同的4kb整数数组,使它们递增:
int* buffers[10] = ...; // 10 4kb buffers, not next to each other
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 512; j++) {
buffers[i][j]++;
}
}
编译器/ CPU非常酷,并且可以为内部循环执行一些缓存预取。棒极了。但是...
...我刚刚吃掉了40kb的缓存,并踢出了我程序其余部分在缓存中享受的数据。
如果我可以向编译器或CPU暗示“在可预见的将来我不会再触摸此内存,以便您可以重复使用这些缓存行”,那将是很酷的事情:
int* buffers[10] = ...;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 512; j++) {
buffers[i][j]++;
}
// Unfetch entire 4kb buffer
cpu_cache_unfetch(buffers[i], 4096);
}
cpu_cache_unfetch从概念上讲会“缩小”该范围内的所有缓存行,并首先将其丢弃。
最后,这意味着我的一小段代码使用4kb缓存,而不是40kb。它将重用4kb的缓存10次。该程序的其余部分将非常感谢。
这是否还有意义?如果是这样,有办法吗?
也非常感谢:让我知道我向自己展示的从根本上误解了缓存的所有方式! = D
答案 0 :(得分:2)
我只知道x86的答案。这绝对是特定于体系结构的;不同的ISA具有不同的缓存控制功能。
在x86上,是的,clflush
/ clflushopt
,但是它们每次执行仅逐出一个缓存行。 (它们 force 写回+逐出,就像您需要内存映射的非易失性存储一样)。我的理解是,clflushopt
在这种情况下通常不值得,而仅仅是允许缓存污染发生。
理论上,将NT预取用于只读可能会提高速度,但这是脆弱的(调整软件预取取决于硬件,而弄错它可能会造成很大的伤害)。进行常规存储可能会消除NT预取的影响,并使该行位于L1,L2和L3中最近使用的位置。
一种可能疯狂的方法是NT商店。加载整个数据缓存行(四个16字节向量= 64字节),然后将更新后的值存储在movntdq
中。
NT表示“非暂时性”;用于在不久的将来(甚至被另一个内核)不再再次引用数据时使用。 What is the meaning of "non temporal" memory accesses in x86有一些非常通用的答案,但可能会有所帮助。
根据Intel的手册,如果先前已缓存目标缓存行(What happens with a non-temporal store if the data is already in cache?),则NT存储区会将其移出目标缓存行,因此它适用于您的用例。但是编译器必须确保在内部循环中达到64字节的对齐边界,以便它可以读取一个或两个完整的缓存行,而不是读取一个32字节的缓存行和另一个32字节的缓存行,并用NT逐出在读取一行的最后32个字节之前存储。 (不过,在asm中,指针数学很容易;编译器确实知道如何将标量转换为对齐边界。)
NT商店的常规用例是for write-only destination buffers to avoid the MESI RFO overhead,但这种用例至少可能是一个胜利。
请参阅注释中的讨论:这可能会严重恶化。在执行此操作之前,一定要先对这两种方法进行基准测试,最好在包括多路插座系统在内的各种硬件上进行测试。
如果数组开始时在缓存中很热,则肯定会更差。我以为这是唯一触动它的东西,而不是修饰链中的 last 。