繁体   English   中英

如果在 Intel Skylake CPU 上调用 function,为什么我的空循环运行速度会快两倍?

[英]Why does my empty loop run twice as fast if called as a function, on Intel Skylake CPUs?

我正在运行一些测试来比较 C 和 Java 并遇到了一些有趣的事情。 在 main 调用的 function 中运行与优化级别 1 (-O1) 完全相同的基准代码,而不是在 main 本身中,导致性能大约翻倍。 我正在打印 test_t 的大小,以毫无疑问地验证代码是否正在编译为 x64。

我将可执行文件发送给正在运行 i7-7700HQ 的朋友并得到了类似的结果。 我正在运行 i7-6700。


这是较慢的代码:

#include <stdio.h>
#include <time.h>
#include <stdint.h>

int main() {
    printf("Size = %I64u\n", sizeof(size_t));
    int start = clock();
    for(int64_t i = 0; i < 10000000000L; i++) {
        
    }
    printf("%ld\n", clock() - start);
    return 0;
}

而且更快:

#include <stdio.h>
#include <time.h>
#include <stdint.h>

void test() {
    printf("Size = %I64u\n", sizeof(size_t));
    int start = clock();
    for(int64_t i = 0; i < 10000000000L; i++) {
        
    }
    printf("%ld\n", clock() - start);
}

int main() {
    test();
    return 0;
}

我还将提供汇编代码供您深入研究。 我不知道组装。 慢点:

    .file   "dummy.c"
    .text
    .def    __main; .scl    2;  .type   32; .endef
    .section .rdata,"dr"
.LC0:
    .ascii "Size = %I64u\12\0"
.LC1:
    .ascii "%ld\12\0"
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    pushq   %rbx
    .seh_pushreg    %rbx
    subq    $32, %rsp
    .seh_stackalloc 32
    .seh_endprologue
    call    __main
    movl    $8, %edx
    leaq    .LC0(%rip), %rcx
    call    printf
    call    clock
    movl    %eax, %ebx
    movabsq $10000000000, %rax
.L2:
    subq    $1, %rax
    jne .L2
    call    clock
    subl    %ebx, %eax
    movl    %eax, %edx
    leaq    .LC1(%rip), %rcx
    call    printf
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbx
    ret
    .seh_endproc
    .ident  "GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0"
    .def    printf; .scl    2;  .type   32; .endef
    .def    clock;  .scl    2;  .type   32; .endef

快点:

    .file   "dummy.c"
    .text
    .section .rdata,"dr"
.LC0:
    .ascii "Size = %I64u\12\0"
.LC1:
    .ascii "%ld\12\0"
    .text
    .globl  test
    .def    test;   .scl    2;  .type   32; .endef
    .seh_proc   test
test:
    pushq   %rbx
    .seh_pushreg    %rbx
    subq    $32, %rsp
    .seh_stackalloc 32
    .seh_endprologue
    movl    $8, %edx
    leaq    .LC0(%rip), %rcx
    call    printf
    call    clock
    movl    %eax, %ebx
    movabsq $10000000000, %rax
.L2:
    subq    $1, %rax
    jne .L2
    call    clock
    subl    %ebx, %eax
    movl    %eax, %edx
    leaq    .LC1(%rip), %rcx
    call    printf
    nop
    addq    $32, %rsp
    popq    %rbx
    ret
    .seh_endproc
    .def    __main; .scl    2;  .type   32; .endef
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    subq    $40, %rsp
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
    call    test
    movl    $0, %eax
    addq    $40, %rsp
    ret
    .seh_endproc
    .ident  "GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0"
    .def    printf; .scl    2;  .type   32; .endef
    .def    clock;  .scl    2;  .type   32; .endef

这是我的编译批处理脚本:

@echo off
set /p file= File to compile: 
del compiled.exe
gcc -Wall -Wextra -std=c17 -O1 -o compiled.exe %file%.c
compiled.exe
PAUSE

对于编译到汇编:

@echo off
set /p file= File to compile: 
del %file%.s
gcc -S -Wall -Wextra -std=c17 -O1 %file%.c
PAUSE

慢速版:

在此处输入图像描述

请注意sub rax, 1 \ jne对正好穿过..80的边界(这是一个 32 字节的边界)。 这是英特尔文档中提到的有关此问题的案例之一,即如下图:

在此处输入图像描述

所以这个 op/branch 对受到JCC 错误修复的影响(这会导致它不被缓存在 µop 缓存中)。 我不确定这是否是原因,还有其他事情在起作用,但这是一回事。

在快速版本中,分支没有“触及” 32 字节边界,因此不受影响。

在此处输入图像描述

可能还有其他适用的效果。 仍然由于跨越 32 字节边界,在较慢的情况下,循环分布在 µop 缓存中的 2 个块上,即使没有修复 JCC 错误,如果循环无法从循环执行,可能导致它在每次迭代中运行 2 个循环Stream 检测器(在某些处理器上被其他错误的其他修复程序 SKL150 禁用)。 参见例如这个关于循环性能的答案。

为了解决各种评论说他们无法重现这一点,是的,有多种可能发生的方式:

  • 无论哪种影响导致速度变慢,都可能是由于操作/分支对跨 32 字节边界的确切位置造成的,这纯属偶然。 从源代码编译不太可能重现相同的情况,除非您使用与原始发布者使用相同设置的相同编译器。
  • 即使使用相同的二进制文件,无论哪个效果负责,奇怪的效果只会发生在特定的处理器上。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM