Intel JCC Erratum - 用于缓解的前缀有什么影响?

Intel JCC Erratum - what is the effect of prefixes used for mitigation?

Intel recommends 使用指令前缀来减轻 JCC Erratum 的性能后果。

如果使用 /QIntel-jcc-erratum 编译的 MSVC 遵循建议,并插入前缀指令,如下所示:

3E 3E 3E 3E 3E 3E 3E 3E 3E 48 8B C8   mov rcx,rax ; with redundant 3E prefixes

They say 当前缀不可用时,MSVC 求助于 NOP。

Clang 对此有 -mbranches-within-32B-boundaries 选项,如果需要,它更喜欢 nop,多字节(https://godbolt.org/z/399nc5Msq 注意 xchg ax, ax

3E前缀的后果是什么,具体来说:

NOP 是一条单独的指令,必须单独解码并通过管道。 总是 最好用前缀填充指令以实现所需的对齐,而不是插入 NOP,如 中所讨论的(但仅限于不会导致某些 CPU 出现重大停顿的方式无法处理大量前缀)。

也许英特尔认为工具链在这种情况下这样做是值得的,因为这实际上是在内循环内部,而不仅仅是内循环外的 NOP。 (并且在前一条指令上添加前缀相对简单。)


我现在有了一些数据点。 /QIntel-jcc-erratum 在 AMD FX 8300 上的基准测试结果是 不好

特定基准测试的减速幅度为十进制数量级,同一基准测试在 Intel Skylake 上的优势约为 20%。这与彼得的评论一致:

I checked Agner Fog's microarch guide, and AMD Zen has no problem with any number of prefixes on a single instruction, like mainstream Intel since Core2. AMD Bulldozer-family has a "very large" penalty for decoding instructions with more than 3 prefixes, like 14-15 cycles for 4-7 prefixes

It's somewhat valid to consider Bulldozer-family obsolete enough to not care much about it, although there are still some APU desktops and laptops around for sure, but they'd certainly show large regressions in loops where the compiler put 4 or more prefixes on one instruction inside a hot inner loop (including existing prefixes like REX or 66h). Much worse than the 3% for MITE legacy decode on SKL.

虽然 Bulldozer-family 确实过时了,但我认为我无法承受如此大的影响。我也担心其他 CPU 可能会以同样的方式被额外的前缀阻塞。所以我的结论是 /QIntel-jcc-erratum 用于一般目标软件。除非在特定的翻译单元中启用它并进行动态调度,否则大多数时候这太麻烦了。


在 MSVC 上可能安全的一件事是停止使用 /Os 标志。发现/Os标志至少:

  • 避免跳转表以支持条件跳转
  • 避免循环开始填充

尝试以下示例 (https://godbolt.org/z/jvezPd9jM):

void loop(int i, char a[], char b[])
{
    char* stop = a + i;
    while (a != stop){
        *b++ = *a++;
    }
}

void jump_table(int i, char a[], char b[])
{
    switch (i)
    {
                            case 7: 
            a[6] = b[6];    case 6: 
            a[5] = b[5];    case 5: 
            a[4] = b[4];    case 4: 
            a[3] = b[3];    case 3: 
            a[2] = b[2];    case 2: 
            a[1] = b[1];    case 1: 
            a[0] = b[1];    case 0:  break;
            default: __assume(false);
    }
}

这会导致 运行 更频繁地进入 JCC 性能问题(避免跳转表会产生一系列 JCC,避免对齐会使小于 16b 的小循环有时也会触及边界)