【问题标题】:How do non temporal instructions work?非临时指令如何工作?
【发布时间】:2012-12-15 21:37:21
【问题描述】:

我正在阅读 Ulrich Drepper 的 What Every Programmer Should Know About Memory pdf。在第 6 部分的开头有一段代码:

#include <emmintrin.h>
void setbytes(char *p, int c)
{
    __m128i i = _mm_set_epi8(c, c, c, c,
    c, c, c, c,
    c, c, c, c,
    c, c, c, c);
    _mm_stream_si128((__m128i *)&p[0], i);
    _mm_stream_si128((__m128i *)&p[16], i);
    _mm_stream_si128((__m128i *)&p[32], i);
    _mm_stream_si128((__m128i *)&p[48], i);
}

下面有这样的评论:

假设指针p 正确对齐,调用此 函数将将寻址缓存行的所有字节设置为c。这 写组合逻辑将看到四个生成的 movntdq 指令 并且仅在最后一次发出内存写入命令 指令已执行。总而言之,这个代码序列不是 只有避免在写入之前读取缓存行,它还 避免用可能很快不需要的数据污染缓存。

让我感到困扰的是,在对函数的注释中写道,它“会将寻址缓存行的所有字节设置为 c”,但根据我对流内部的理解,它们绕过缓存 - 既没有缓存读取,也没有缓存写入。此代码将如何访问任何缓存行?第二个粗体片段表示类似,该函数“避免在写入之前读取缓存行”。如上所述,我看不到任何缓存的写入方式和时间。此外,是否需要在缓存写入之前对缓存进行任何写入?有人可以向我澄清这个问题吗?

【问题讨论】:

  • 您对 SSE 缓存操作的假设是否有参考? Intel documentation 指定了 Ulrich 在评论中引用的污染。
  • 我的知识全部来自 Ulrich 的论文。在本章的前面,他写道:“这些非临时写操作不会读取缓存行然后对其进行修改;而是将新内容直接写入内存。”。它来自“6.1 绕过缓存”部分的第二段
  • 我不清楚他想说什么,但 MOVNTDQ 确实更新缓存,如果它恰好包含地址。
  • @HansPassant: movntdq 可以命中缓存,但 it evicts the line from cache if it was present,根据英特尔手册 vol1 ch 10.4.6.2 缓存临时数据与非临时数据。我猜这个设计决定是为了让驱动程序可以在 NT 存储到视频内存或其他东西之后避免clflush。 (IIRC,文档说这种保证驱逐不会发生在早期的 CPU 上以支持该指令。)
  • _mm_set1_epi8(c) 比输入c 16 次更容易广播一个字节。

标签: memory x86 cpu-architecture intrinsics cpu-cache


【解决方案1】:

我认为这部分是一个术语问题:您从 Ulrich Drepper 的文章中引用的段落不是在谈论缓存数据。它只是使用术语“缓存线”来表示对齐的 64B 块。

这是正常的,在讨论具有不同缓存行大小的一系列硬件时尤其有用。 (早期的 x86 CPU,以及最近的 PIII,有 32B 缓存线,所以使用这个术语可以避免将微架构设计决策硬编码到讨论中。)

数据的缓存行仍然是缓存行,即使它当前在任何缓存中都不热。

【讨论】:

    【解决方案2】:

    写入内存时,必须先将写入的缓存行加载到缓存中,以防您只写入部分缓存行。

    当您写入内存时,存储在存储缓冲区中分组。通常,一旦缓冲区已满,它将被刷新到缓存/内存。请注意,存储缓冲区的数量通常很小(~4)。连续写入地址将使用相同的存储缓冲区。

    带有非临时提示的流式读/写通常用于减少缓存污染(通常使用 WC 内存)。这个想法是在 CPU 上保留一小组缓存线供这些指令使用。不是将缓存行加载到主缓存中,而是将其加载到这个较小的缓存中。

    评论假设以下行为(但我找不到硬件实际执行此操作的任何参考,需要测量或可靠来源,并且它可能因硬件而异): - 一旦 CPU 发现存储缓冲区已满并且与缓存行对齐,它将直接将其刷新到内存,因为非临时写入绕过了主缓存。

    唯一可行的方法是,如果存储缓冲区与实际写入的缓存行在刷新后发生合并。这是一个合理的假设。

    注意,如果写入的缓存行已经在主缓存中,上述方法也会更新它们。

    如果使用常规内存写入而不是非临时写入,则存储缓冲区刷新也会更新主缓存。这种情况完全有可能避免读取内存中的原始缓存行。

    如果使用非临时写入写入部分缓存行,则可能需要从主内存(或主缓存,如果存在)中获取缓存行,如果我们没有读取缓存行,可能会非常慢提前进行常规读取或非临时读取(这会将其放入我们单独的缓存中)。

    通常,非临时缓存大小约为 4-8 个缓存行。

    总而言之,最后一条指令开始写入,因为它也恰好填满了存储缓冲区。存储缓冲区刷新可以避免读取写入的高速缓存行,因为硬件知道存储缓冲区是连续的并且与高速缓存行对齐。非临时写入提示仅用于避免使用我们写入的缓存行 IF 填充主缓存,并且仅当它尚未在主缓存中时。

    【讨论】:

    • NT 在缓存中命中的存储会驱逐缓存行。 IIRC,当前的英特尔设计每个内核有 10 个填充缓冲区。您的 4-8 可能来自旧的信息来源,或者我记错了。但是,是的,据我所知,完全填充填充缓冲区会触发它刷新的总体观点是正确的。
    【解决方案3】:

    我没有参考资料来证明我在说什么,但我的理解是:内存总线上的唯一传输单元是高速缓存行,无论它们进入高速缓存还是某些特殊寄存器。所以确实,您粘贴的代码填充了一个缓存行,但它是一个不驻留在缓存中的特殊缓存行。一旦这个缓存行的所有字节都被修改,缓存行会直接发送到内存,而不经过缓存。

    【讨论】:

      猜你喜欢
      • 2019-01-29
      • 1970-01-01
      • 2018-03-19
      • 2022-01-20
      • 1970-01-01
      • 2015-01-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多