如何在我的 C 代码中使用 PREFETCHT0 指令?

How to use PREFETCHT0 Instruction in my C code?

我想在我的 C 程序中预取某些地址(这是一个巨大数组的某些元素的地址),并查看它对所用时间的影响。

我找到了关于 PREFETCH 的说明 here PREFETCH0。但我不知道如何使用内联汇编在 C 中使用它。如果有人能给出一些想法,我应该如何在 C 程序中将地址作为参数使用这条指令,那将会很有帮助。

不要使用内联汇编编写它,这会使编译器的工作变得更加困难。 GCC 有一个内置的扩展(参见 gcc builtins docs 了解更多细节)你应该使用预取:

__builtin_prefetch(const void*)

这将使用目标的预取指令生成代码,但编译器的智能范围更大。

作为内联 ASM 和 gcc 内置函数之间区别的简单示例,请考虑以下两个文件,test1.c:

void foo(double *d, unsigned len) {
  for (unsigned i = 0; i < len; ++i) {
    __builtin_prefetch(&d[i]);
    d[i] = d[i] * d[i];
  }
}

和test2.c:

void foo(double *d, unsigned len) {
  for (unsigned i = 0; i < len; ++i) {
    asm("prefetcht0 (%0)" 
        : /**/
        : "g"(&d[i])
        : /**/
    );
    d[i] = d[i] * d[i];
  }
}

(请注意,如果您进行基准测试,我 99% 确定没有预取的第三个版本会比上述两个版本更快,因为您有可预测的访问模式,所以唯一真正实现的是添加更多的指令字节和更多的周期)

如果我们在 x86_64 上使用 -O3 编译两者并比较生成的输出,我们会看到:

        .file   "test1.c"                                       |          .file   "test2.c"
        .text                                                              .text
        .p2align 4,,15                                                     .p2align 4,,15
        .globl  foo                                                        .globl  foo
        .type   foo, @function                                             .type   foo, @function
foo:                                                               foo:
.LFB0:                                                             .LFB0:
        .cfi_startproc                                                     .cfi_startproc
        testl   %esi, %esi      # len                                      testl   %esi, %esi      # len
        je      .L1     #,                                                 je      .L1     #,
        leal    -1(%rsi), %eax  #, D.1749                       |          leal    -1(%rsi), %eax  #, D.1745
        leaq    8(%rdi,%rax,8), %rax    #, D.1749               |          leaq    8(%rdi,%rax,8), %rax    #, D.1745
        .p2align 4,,10                                                     .p2align 4,,10
        .p2align 3                                                         .p2align 3
.L4:                                                               .L4:
        movsd   (%rdi), %xmm0   # MEM[base: _8, offset: 0B], D. |  #APP
        prefetcht0      (%rdi)  # ivtmp.6                       |  # 3 "test2.c" 1
                                                                >          prefetcht0 (%rdi)       # ivtmp.6
                                                                >  # 0 "" 2
                                                                >  #NO_APP
                                                                >          movsd   (%rdi), %xmm0   # MEM[base: _8, offset: 0B], D.
        addq    , %rdi        #, ivtmp.6                                 addq    , %rdi        #, ivtmp.6
        mulsd   %xmm0, %xmm0    # D.1748, D.1748                |          mulsd   %xmm0, %xmm0    # D.1747, D.1747
        movsd   %xmm0, -8(%rdi) # D.1748, MEM[base: _8, offset: |          movsd   %xmm0, -8(%rdi) # D.1747, MEM[base: _8, offset:
        cmpq    %rax, %rdi      # D.1749, ivtmp.6               |          cmpq    %rax, %rdi      # D.1745, ivtmp.6
        jne     .L4     #,                                                 jne     .L4     #,
.L1:                                                               .L1:
        rep ret                                                            rep ret
        .cfi_endproc                                                       .cfi_endproc
.LFE0:                                                             .LFE0:
        .size   foo, .-foo                                                 .size   foo, .-foo
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"               .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
        .section        .note.GNU-stack,"",@progbits                       .section        .note.GNU-stack,"",@progbits

即使在这种简单的情况下,有问题的编译器 (GCC 4.8.4) 也利用了这样一个事实,即它允许重新排序并选择,大概是基于目标处理器的内部模型,移动预取 初始加载发生后。如果我不得不猜测在某些情况下按该顺序进行加载和预取会稍微快一些。据推测,此顺序对未命中和命中的惩罚较低。或者像这样的排序更适合分支预测。不过,编译器为什么选择这样做并不重要,关键是 非常 复杂,要完全理解在实际应用程序中对生成的代码进行哪怕是微不足道的更改对现代处理器的影响.通过使用内置函数而不是内联汇编,您可以从今天的编译器知识和将来出现的任何改进中获益。即使您花费两周的时间研究这个简单的案例并对其进行基准测试,您也很有可能无法击败未来的编译器,并且您甚至可能最终得到一个无法从未来的改进中受益的代码库。

这些问题甚至在我们开始讨论您的代码的可移植性之前就出现了 - 对于内置函数,当在不支持优雅降级或启用仿真的体系结构上时,它们通常属于两类之一。当它出现时,具有大量 x86 内联汇编的应用程序更难移植到 x86_64。

您可以在某些 asm 代码中添加一些 PREFETCH* 汇编程序指令,请参阅 How to Use Assembly Language in C code

但是,您应该更喜欢(因为 ) using the __builtin_prefetch because it is a compiler internal builtin(并且它确实在预取地址之后接受两个可选的附加参数)并且编译器比您在 asm 中提供的更了解它指令。因此它可能会相应地优化您的其余代码。

另请参阅 this and that 答案。添加太多(或错误地)一些预取指令会减慢您的程序,因此您应该以简约的方式使用它(甚至可能根本不使用)。一定要进行基准测试(并要求优化,例如 gcc -O2 -march=native ...)。启发式地,您想要预取数据 "in advance"(例如,对于接下来的 5 或 10 次迭代)。

假设您有一项时间紧迫任务(包括使用XOR)和编译器不是或 永远不会 最优。

完成后,我将使用更复杂问题的时间测量来更新此答案。这是解决该问题的唯一答案,所有其他答案基本上都在说 "don't do it"。请参阅下面来自 Agner 的引述。

//  CLOCK_MONOTONIC_COARSE
//  CLOCK_MONOTONIC_RAW

#define DO_SOMETHING_ELSE_BEFORE_LOADING(i)     \
asm volatile (                                  \
        "movl        00000, %%ecx        ; " \
        "prefetcht0  (%%rax)                ; " \
        "for:                               ; " \
        "pxor        %%xmm0,  %%xmm1        ; " \
        "dec         %%ecx                  ; " \
        "jnz for                            ; " \
        "movdqa      (%%rax),  %%xmm0       ; " \
        :                                       \
        :                                       \
        : "%rax", "%ecx", "%xmm0", "%xmm1"      \
);

int main() {
    DO_SOMETHING_ELSE_BEFORE_LOADING(i)
    return 0;
}

以下内容看起来像是一个很好的资源,并且基本上断言与其他答案完全相反

上面写着

    1. 优化代码以提高速度。在大多数情况下,现代 C++ 编译器通常可以很好地优化代码。但是仍然存在编译器性能不佳的情况,并且可以通过仔细的汇编编程来显着提高速度。

生成文件

SET(CMAKE_CXX_FLAGS "-std=gnu++11 -march=native -mtune=native -msse2")