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

时间:2016-11-05 06:57:06

标签: c linux assembly x86 inline-assembly

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

关于PREFETCH的指令我找到了here PREFETCH0。但我不知道如何使用内联汇编在C中使用它。如果某个机构能够在C程序中如何使用该指令和地址作为参数,那将是非常有帮助的。

3 个答案:

答案 0 :(得分:6)

不要使用内联汇编来编写它,这会使编译器的工作更难。 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    $8, %rdi        #, ivtmp.6                                 addq    $8, %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。

答案 1 :(得分:2)

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

但是,你应该更喜欢(Flexo answered)使用__builtin_prefetch,因为它是一个编译器内部builtin(它在预取的地址之后接受两个可选的附加参数)和编译器比你在asm指令中提供的内容更了解它。因此,它可能会相应地优化其余代码。

另请参阅thisthat个答案。添加太多(或错误地)一些预取指令可能会降低程序的速度,因此您应该将它与简约一起使用(甚至可能根本不使用)。一定要进行基准测试(并要求进行优化,例如gcc -O2 -march=native ...)。启发式地,你想提前预取数据&#34;&#34; (例如,接下来的5或10次迭代)。

答案 2 :(得分:-3)

假设您有时间关键任务(包括使用 XOR )并且编译器没有或从不< / strong>最佳。

我会在完成时通过更复杂问题的时间测量来更新此答案。这是解决问题的唯一答案,所有其他答案基本上都在说,&#34;不要这样做&#34;。请参阅下面的Agner引用。

//  CLOCK_MONOTONIC_COARSE
//  CLOCK_MONOTONIC_RAW

#define DO_SOMETHING_ELSE_BEFORE_LOADING(i)     \
asm volatile (                                  \
        "movl        $1000000, %%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")