GCC vs CLANG 指向 char* 优化的指针

Posted

技术标签:

【中文标题】GCC vs CLANG 指向 char* 优化的指针【英文标题】:GCC vs CLANG pointer to char* optimization 【发布时间】:2017-09-09 09:15:16 【问题描述】:

我有这个代码: 主PC:

int main(int c, char **v)
        char *s = v[0];
        while (*s++ != 0) 
                if ((*s == 'a') && (*s != 'b')) 
                        return 1;
                
        
        return 0;

我用 clang 和 gcc 编译生成汇编代码来比较优化:

clang-3.9 -S -masm=intel -O3 mainP.c
gcc -S -masm=intel -O3 mainP.c

编译器版本为:

clang version 3.9.1-9 (tags/RELEASE_391/rc2)
Target: x86_64-pc-linux-gnu
gcc (Debian 6.3.0-18) 6.3.0 20170516

生成的 2 个汇编代码是:

gcc 汇编代码:

main:
.LFB0:
        .cfi_startproc
        mov     rax, QWORD PTR [rsi]
        jmp     .L2
        .p2align 4,,10
        .p2align 3
.L4:
        cmp     BYTE PTR [rax], 97
        je      .L5
.L2:
        add     rax, 1
        cmp     BYTE PTR -1[rax], 0
        jne     .L4
        xor     eax, eax
        ret
.L5:
        mov     eax, 1
        ret

clang 汇编代码:

main:                                   # @main
        .cfi_startproc
# BB#0:
        mov     rcx, qword ptr [rsi]
        mov     dl, byte ptr [rcx]
        inc     rcx
        .p2align        4, 0x90
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        xor     eax, eax
        test    dl, dl
        je      .LBB0_3
# BB#2:                                 #   in Loop: Header=BB0_1 Depth=1
        movzx   edx, byte ptr [rcx]
        inc     rcx
        mov     eax, 1
        cmp     dl, 97
        jne     .LBB0_1
.LBB0_3:
        ret

我注意到:在 gcc 汇编代码中,*s 在循环中被访问两次,而 *s 在 clang 汇编代码中只被访问一次。

有什么区别的解释吗?

然后在稍微更改 C 代码(添加一个本地 char 变量)后,我得到的汇编代码与 GCC 大致相同:

int main(int c, char **v)
        char *s = v[0];
        char ch;
        ch = *s;
        while (ch != 0) 
                if ((ch == 'a') && (ch != 'b')) 
                        return 1;
                
                ch = *s++;
        
        return 0;

使用 GCC 生成的汇编代码:

main:
.LFB0:
        .cfi_startproc
        mov     rax, QWORD PTR [rsi]
        movzx   edx, BYTE PTR [rax]
        test    dl, dl
        je      .L6
        add     rax, 1
        cmp     dl, 97
        jne     .L5
        jmp     .L8
        .p2align 4,,10
        .p2align 3
.L4:
        cmp     dl, 97
        je      .L8
.L5:
        add     rax, 1
        movzx   edx, BYTE PTR -1[rax]
        test    dl, dl
        jne     .L4
.L6:
        xor     eax, eax
        ret
.L8:
        mov     eax, 1
        ret

【问题讨论】:

s++ 出现在两个 C++ 代码片段的不同位置。 clang 只需省略第二次读取。无论这好坏,都应该对代码进行更仔细的分析。你是在问谁表现更好或更玄学为什么 我不是在问哪个更好。这取决于情况,如果 gcc 代码中两次读取之间的内存发生变化,则与运行 clang 代码相比,行为会有所不同。 第二次读取是*s++,困难在于它并不总是第二次读取(第一次迭代)。编译器需要将 *(c?a:b) 转换为 c?*a:*b 并注意 *b 在采用该分支时是多余的,在这种情况下 gcc 会丢失。随时向 gcc 的 bugzilla 报告这个错过的优化。 关于 GCC 的错误报告链接:gcc.gnu.org/bugzilla/show_bug.cgi?id=82187 【参考方案1】:

差异的解释:编译器优化是困难,不同的编译器会以不同的方式优化你的代码。

因为表达式比较简单,我们可以假设*s在两个地方都是相同的值,只需要一个负载。

但是弄清楚优化是棘手的,因为编译器必须绝对“知道”在第一个引用和第二个引用之间不能更改“任何 s 指向的内容”。

在你的例子中,clang 比 gcc 更擅长解决这个问题。

【讨论】:

gcc 是否有一个选项,以便它从第一个 c 代码生成汇编代码,只有一次访问,如 clang?此外,生成的汇编代码在使用不同的编译器版本编译时肯定会发生变化。跨度> 我不知道是否有编译器选项,而且版本之间存在差异我并不感到惊讶——每个版本几乎总是对优化器进行改进/更改。 如果您想强制进行一次访问,我建议您在 C 代码中使用中间变量,就像您在第二个示例中所做的那样。 并不是 gcc 认为它没有意识到它是同一个位置并且它可以假设它没有被修改,而是它没有将循环管道/转换为特殊情况下的零长度字符串,然后能够在源循环的下一次迭代中为!=0 重用加载的字符。 ***.com/questions/46129083/…。 gcc/clang 也不会像这样在第一次迭代之前无法确定迭代计数的自动矢量化(使用 SSE2)搜索循环,所以我认为条件 return 是有问题的。 其实问题不在于条件返回。计算整个字符串的匹配可以避免这种情况,但它仍然是一个未知的迭代计数,并且 gcc 仍然会加载两次 godbolt.org/g/vnshJL。

以上是关于GCC vs CLANG 指向 char* 优化的指针的主要内容,如果未能解决你的问题,请参考以下文章

为啥 gcc 会产生这个奇怪的程序集 vs clang?

为啥这个结构文字在 VS2013 中通过地址而不是 gcc/clang 时会损坏?

Clang vs GCC - 产生更快的二进制文件? [关闭]

从通用 lambda 调用 `this` 成员函数 - clang vs gcc

可以跨 C 和 C++ 方法优化 gcc 或 clang 的 LTO

为啥在 gcc 和 clang 上通过优化将大 double 转换为 uint16_t 会给出不同的答案