82

所以,我有这个代码:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        *sum++ = *a++ + *b++;
    }
}

我想看看 GCC 4.7.2 会生成的代码。所以我跑了g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11,得到了以下输出:

        .file   "a.c++"
        .intel_syntax noprefix
        .text
        .p2align 4,,15
        .globl  _Z2f1PcS_S_
        .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L5
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L5
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L3:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L3
        mov     ax, 8
        mov     r9d, 992
.L2:
        sub     eax, 1
        lea     rcx, [rdx+r9]
        add     rdi, r9
        lea     r8, [rax+1]
        add     rsi, r9
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L4:
        movzx   edx, BYTE PTR [rcx+rax]
        add     dl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], dl
        add     rax, 1
        cmp     rax, r8
        jne     .L4
        rep
        ret
.L5:
        mov     eax, 1000
        xor     r9d, r9d
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
        .p2align 4,,15
        .globl  _Z2f2PcS_S_
        .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L19
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L19
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L17:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L17
        add     rdi, 992
        add     rsi, 992
        add     rdx, 992
        mov     r8d, 8
.L16:
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L18:
        movzx   ecx, BYTE PTR [rdx+rax]
        add     cl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], cl
        add     rax, 1
        cmp     rax, r8
        jne     .L18
        rep
        ret
.L19:
        mov     r8d, 1000
        jmp     .L16
        .cfi_endproc
.LFE1:
        .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
        .ident  "GCC: (GNU) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

我不擅长阅读汇编,所以我决定添加一些标记来了解循环体的去向:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        asm("# im in ur loop");
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        asm("# im in ur loop");
        *sum++ = *a++ + *b++;
    }
}

GCC 吐出了这个:

    .file   "a.c++"
    .intel_syntax noprefix
    .text
    .p2align 4,,15
    .globl  _Z2f1PcS_S_
    .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L2:
#APP
# 4 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L2
    rep
    ret
    .cfi_endproc
.LFE0:
    .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
    .p2align 4,,15
    .globl  _Z2f2PcS_S_
    .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L6:
#APP
# 12 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L6
    rep
    ret
    .cfi_endproc
.LFE1:
    .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
    .ident  "GCC: (GNU) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

这要短得多,并且有一些显着差异,例如缺少 SIMD 指令。我期待相同的输出,中间有一些评论。我在这里做了一些错误的假设吗?GCC 的优化器是否受到 asm 注释的阻碍?

4

4 回答 4

63

与优化的交互在文档中的“带有 C 表达式操作数的汇编器指令”页面的中途进行了解释。

GCC 不会尝试理解asm; 中的任何实际程序集。它唯一知道的内容是您(可选地)在输出和输入操作数规范以及寄存器破坏列表中告诉它的内容。

特别要注意:

没有任何输出操作数的asm指令将被视为与 volatileasm指令相同。

关键字表示该volatile指令具有重要的副作用 [...]

因此,asm循环内部的存在抑制了矢量化优化,因为 GCC 假设它有副作用。

于 2012-12-19T15:18:22.663 回答
23

请注意,gcc 将代码向量化,将循环体分成两部分,第一部分一次处理 16 个项目,第二部分稍后处理其余部分。

正如 Ira 评论的那样,编译器不解析 asm 块,所以它不知道它只是一个注释。即使这样做了,它也无法知道您的意图。优化的循环使主体加倍,是否应该将您的 asm 放入每个循环中?您希望它不执行 1000 次吗?它不知道,所以它走安全路线并退回到简单的单循环。

于 2012-12-19T15:14:46.143 回答
3

我不同意“gcc 不理解asm()块中的内容”。例如,gcc 可以很好地处理优化参数,甚至可以重新排列asm()块,使其与生成的 C 代码混合。这就是为什么,如果您查看例如 Linux 内核中的内联汇编程序,它几乎总是带有前缀__volatile__以确保编译器“不会移动代码”。我让 gcc 移动了我的“rdtsc”,这让我测量了做某件事所花费的时间。

如文档所述,gcc 将某些类型的asm()块视为“特殊”,因此不会优化块两侧的代码。

这并不是说 gcc 有时不会被内联汇编程序块弄糊涂,或者只是决定放弃某些特定的优化,因为它无法遵循汇编程序代码的后果等等。更重要的是,它经常会因为缺少clobber标签而感到困惑——所以如果你有一些指令,比如cpuid这会改变 EAX-EDX 的值,但是您编写的代码只使用 EAX,编译器可能会将内容存储在 EBX、ECX 和 EDX 中,然后当这些寄存器被覆盖时,您的代码会表现得很奇怪......如果你很幸运,它会立即崩溃 - 然后很容易弄清楚发生了什么。但是如果你不走运,它就会崩溃……另一个棘手的问题是在 edx 中给出第二个结果的除法指令。如果您不关心模数,很容易忘记 EDX 已更改。

于 2012-12-20T16:31:11.257 回答
-2

这个答案现在被修改了:它最初是用一种思维方式编写的,将内联基本 Asm 视为一个非常明确的工具,但它与 GCC 中的完全不同。Basic Asm 很弱,因此编辑了答案。

每个程序集注释都充当断点。

编辑:但是一个坏的,当你使用基本 Asm 时。没有显式 clobber 列表的内联asmasm函数体内的语句)是 GCC 中的一个弱指定特性,它的行为很难定义。它似乎没有我没有完全掌握它的保证)附加到任何特别的东西,所以虽然如果函数运行,汇编代码必须在某个时候运行,但不清楚它何时运行任何非微不足道的优化级别。可以用相邻指令重新排序的断点不是一个非常有用的“断点”。结束编辑

您可以在解释器中运行程序,该解释器会在每个注释处中断并打印出每个变量的状态(使用调试信息)。这些点必须存在,以便您观察环境(寄存器和内存的状态)。

如果没有注释,则不存在观察点,并且循环被编译为单个数学函数,采用环境并产生修改后的环境。

你想知道一个无意义的问题的答案:你想知道每条指令(或者可能是块,或者可能是指令范围)是如何编译的,但没有单独的指令(或块)被编译;整个东西是作为一个整体编译的。

一个更好的问题是:

你好海湾合作委员会。为什么你认为这个 asm 输出正在实现源代码?请逐步解释,每一个假设。

但是,您不会希望阅读比 asm 输出更长的证明,以 GCC 内部表示形式编写。

于 2015-10-06T02:22:46.410 回答