首页 文章

gcc -O0仍然优化了“未使用”的代码 . 是否有一个编译标志来改变它?

提问于
浏览
46

当我在this question中提出时,gcc正在移除(是的,使用 -O0 )一行代码 _mm_div_ss(s1, s2); ,大概是因为结果未保存 . 但是,这应该触发浮点异常并引发SIGFPE,如果删除调用则不会发生这种情况 .

Question :是否有一个标志或多个标志传递给gcc,以便代码按原样编译?我正在考虑像 fno-remove-unused 这样的东西,但是我支持使用gcc属性/ pragma代替吗?

我尝试过的事情:

$ gcc --help=optimizers | grep -i remove

没有结果 .

$ gcc --help=optimizers | grep -i unused

没有结果 .

并明确禁用所有死代码/消除标志 - 请注意,没有关于未使用代码的警告:

$ gcc -O0 -msse2 -Wall -Wextra -pedantic -Winline \
     -fno-dce -fno-dse -fno-tree-dce \
     -fno-tree-dse -fno-tree-fre -fno-compare-elim -fno-gcse  \
     -fno-gcse-after-reload -fno-gcse-las -fno-rerun-cse-after-loop \
     -fno-tree-builtin-call-dce -fno-tree-cselim a.c
a.c: In function ‘main’:
a.c:25:5: warning: ISO C90 forbids mixed declarations and code [-Wpedantic]
     __m128 s1, s2;
     ^
$

来源计划

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <xmmintrin.h>

static void sigaction_sfpe(int signal, siginfo_t *si, void *arg)
{
    printf("%d,%d,%d\n", signal, si!=NULL?1:0, arg!=NULL?1:0);
    printf("inside SIGFPE handler\nexit now.\n");
    exit(1);
}

int main()
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = sigaction_sfpe;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGFPE, &sa, NULL);

    _mm_setcsr(0x00001D80);

    __m128 s1, s2;
    s1 = _mm_set_ps(1.0, 1.0, 1.0, 1.0);
    s2 = _mm_set_ps(0.0, 0.0, 0.0, 0.0);
    _mm_div_ss(s1, s2);

    printf("done (no error).\n");

    return 0;
}

编译上面的程序给出了

$ ./a.out
done (no error).

换线

_mm_div_ss(s1, s2);

s2 = _mm_div_ss(s1, s2); // add "s2 = "

产生预期的结果:

$ ./a.out
inside SIGFPE handler

编辑更多细节 .

这似乎与 _mm_div_ss definition上的 __always_inline__ 属性有关 .

$ cat t.c
int
div(int b)
{
    return 1/b;
}

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


$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
$

(没有警告或错误)

$ ./t.out
Floating point exception
$

vs下面(功能属性除外)

$ cat t.c
__inline int __attribute__((__always_inline__))
div(int b)
{
    return 1/b;
}

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

$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
$

(没有警告或错误)

$ ./t.out
$

添加函数属性 __warn_unused_result__ 至少会给出一条有用的消息:

$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
t.c: In function ‘main’:
t.c:9:5: warning: ignoring return value of ‘div’, declared with attribute warn_unused_result [-Wunused-result]
     div(0);
     ^

编辑:

关于gcc mailing list的一些讨论 . 最终,我认为一切都按预期工作 .

3 回答

  • 32

    海湾合作委员会不会在这里“优化”任何东西 . 它只是不会生成无用的代码 . 似乎有一种非常普遍的错觉,即编译器应该生成一些纯粹的代码形式,并且对它的任何更改都是“优化” . 哪有这回事 .

    编译器创建一些表示代码含义的数据结构,然后它对该数据结构应用一些转换,然后生成汇编程序,然后编译成指令 . 如果你在没有“优化”的情况下进行编译,那只意味着编译器只会尽可能少地生成代码 .

    在这种情况下,整个语句是无用的,因为它没有做任何事情并且立即被抛弃(在扩展内联之后以及内置函数意味着它等同于编写 a/b; ,区别在于写入 a/b; 将发出关于 statement with no effect 的警告而内置可能不会被相同的警告处理) . 这不是优化,编译器实际上必须花费额外的努力来为无意义的语句创造意义,然后伪造一个临时变量来存储该语句的结果然后将其丢弃 .

    您正在寻找的不是禁用优化的标志,而是禁用标志 . 我不认为任何编译器开发人员浪费时间实现这样的标志 . 除了可能是一个愚人节开玩笑 .

  • 10

    我不是 gcc 内部的专家,但似乎你的问题不在于通过某些优化传递删除死代码 . 编译器很可能甚至不考虑首先生成此代码 .

    让我们将您的示例从编译器特定的内在函数减少到一个普通的旧函数:

    int foo(int num) {
        num + 77;
        return num + 15;
    }
    

    No code for + 77 generated

    foo(int):
            push    rbp
            mov     rbp, rsp
            mov     DWORD PTR [rbp-4], edi
            mov     eax, DWORD PTR [rbp-4]
            add     eax, 15
            pop     rbp
            ret
    

    当其中一个操作数有副作用时,only that operand gets evaluated . 仍然没有在组装中添加 .

    但是将此结果保存到(甚至未使用的)变量会强制编译器generate code for addition

    int foo(int num) {
      int baz = num + 77;
      return num + 15;
    }
    

    部件:

    foo(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        mov     eax, DWORD PTR [rbp-20]
        add     eax, 77
        mov     DWORD PTR [rbp-4], eax
        mov     eax, DWORD PTR [rbp-20]
        add     eax, 15
        pop     rbp
        ret
    

    以下只是一个推测,但根据我在编译器构造方面的经验,不为未使用的表达式生成代码更自然,而不是在以后消除此代码 .

    我的建议是明确你的意图,并将表达式的结果放入volatile(因此,优化器不可删除)变量 .

    @Matthieu M指出,防止预先计算该值是不够的 . 因此,除了使用信号之外,还应该使用记录的方法来执行所需的确切指令(可能是 volatile 内联汇编) .

  • 22

    为什么gcc没有发出指定的指令?

    编译器生成的代码必须具有Standard指定的可观察行为 . 任何不可观察的东西都可以随意改变(和优化),因为它不会改变程序的行为(如指定的那样) .

    你怎么能把它击败?

    诀窍是让编译器相信特定代码片段的行为实际上是可观察的 .

    由于这是微基准测试中经常遇到的问题,我建议您查看(例如)Google-Benchmark如何解决此问题 . 从benchmark_api.h我们得到:

    template <class Tp>
    inline void DoNotOptimize(Tp const& value) {
        asm volatile("" : : "g"(value) : "memory");
    }
    

    this syntax的细节很无聊,为了我们的目的,我们只需要知道:

    • "g"(value) 告诉 value 用作语句的输入

    • "memory" 是编译时读/写屏障

    所以,我们可以将代码更改为:

    asm volatile("" : : : "memory");
    
    __m128 result = _mm_div_ss(s1, s2);
    
    asm volatile("" : : "g"(result) : );
    

    哪一个:

    • 强制编译器认为 s1s2 可能在初始化和使用之间被修改

    • 迫使编译器考虑到使用操作的结果

    不需要任何标志,它应该在任何优化级别上工作(我在https://gcc.godbolt.org/ at -O3上测试它) .

相关问题