【问题标题】:Why does GCC delete my code on O3 but not on O0?为什么 GCC 会删除我在 O3 上的代码,而不是在 O0 上?
【发布时间】:2020-08-11 05:08:41
【问题描述】:

最近我一直在尝试学习右值和完美转发。在玩弄一些结构时,我在切换编译器和优化级别时遇到了一些特殊的行为。

在没有开启优化的情况下在 GCC 上编译相同的代码会产生预期的结果,但是开启任何优化级别会导致我的所有代码都被删除。 在没有优化的情况下在 clang 上编译相同的代码也会产生预期的结果。然后在 clang 上开启优化仍然会产生预期的结果。

我知道这会导致未定义的行为,但我就是无法弄清楚到底出了什么问题以及是什么导致了两个编译器之间的差异。

gcc -O0 -std=c++17 -Wall -Wextra

https://godbolt.org/z/5xY1Gz

gcc -O3 -std=c++17 -Wall -Wextra

https://godbolt.org/z/fE3TE5

clang -O0 -std=c++17 -Wall -Wextra

https://godbolt.org/z/W98fh8

clang -O3 -std=c++17 -Wall -Wextra

https://godbolt.org/z/6sEo8j

#include <utility>

// lambda_t is the type of thing we want to call.
// capture_t is the type of a helper object that 
// contains all all parameters meant to be passed to the callable
template< class lambda_t, class capture_t >
struct CallObject {

    lambda_t  m_lambda;
    capture_t m_args;

    typedef decltype( m_args(m_lambda) ) return_t;

    //Construct the CallObject by perfect forwarding which is
    //neccessary as they may these are lambda which will have
    //captured objects and we dont want uneccessary copies
    //while passing these around
    CallObject( lambda_t&& p_lambda, capture_t&& p_args ) :
        m_lambda{ std::forward<lambda_t>(p_lambda) },
        m_args  { std::forward<capture_t>(p_args) }
    {

    }

    //Applies the arguments captured in m_args to the thing
    //we actually want to call
    return_t invoke() {
        return m_args(m_lambda);
    }

    //Deleting special members for testing purposes
    CallObject() = delete;
    CallObject( const CallObject& ) = delete;
    CallObject( CallObject&& ) = delete;
    CallObject& operator=( const CallObject& ) = delete;
    CallObject& operator=( CallObject&& ) = delete;
};

//Factory helper function that is needed to create a helper
//object that contains all the paremeters required for the 
//callable. Aswell as for helping to properly templatize
//the CallObject
template< class lambda_t, class ... Tn >
auto Factory( lambda_t&& p_lambda, Tn&& ... p_argn ){

    //Using a lambda as helper object to contain all the required paramters for the callable
    //This conviently allows for storing value, references and so on
    auto x = [&p_argn...]( lambda_t& pp_lambda ) mutable -> decltype(auto) {

        return pp_lambda( std::forward<decltype(p_argn)>(p_argn) ... );
    };

    typedef decltype(x) xt;
    //explicit templetization is not needed in this case but
    //for the sake of readability it needed here since we then
    //need to forward the lambda that captures the arguments
    return CallObject< lambda_t, xt >( std::forward<lambda_t>(p_lambda), std::forward<xt>(x) );
}

int main(){

    auto xx = Factory( []( int a, int b ){

        return a+b;

    }, 10, 3 );

    int q = xx.invoke();

    return q;
}

【问题讨论】:

  • I know this screams undefined behavior but i just cannot figure out what exactly is going wrong 你说那句话出了什么问题。程序的行为未定义。
  • 但是为什么呢?我想了解我做错了什么。即使使用 -Wall -Wextra 也没有给我警告

标签: c++


【解决方案1】:

如果发生这种情况,通常是因为您在程序的某个地方有未定义的行为。编译器确实检测到了这一点,并且在积极优化时会因此丢弃整个程序。

在您的具体示例中,您已经以编译器警告的形式得到了一些不太正确的提示:

<source>: In function 'int main()':
<source>:45:18: warning: '<anonymous>' is used uninitialized [-Wuninitialized]
   45 |         return a+b;
      |                  ^

怎么会这样?什么可能导致 b 此时未初始化?

由于此时b 是一个函数参数,所以问题一定出在该lambda 的调用者 上。检查调用站点,我们发现了一些可疑之处:

auto x = [&p_argn...]( lambda_t& pp_lambda ) mutable -> decltype(auto) {
    return pp_lambda( std::forward<decltype(p_argn)>(p_argn) ... );
};

绑定到b 的参数作为参数包p_argn 传递。但请注意该参数包的生命周期:它是通过引用捕获的!因此,尽管您在 lambda 主体中编写了 std::forward,但这里没有完美的转发,因为您在 lambda 中通过引用捕获,而 lambda 不会“看到”在其主体之外在周围函数中发生的事情. a 这里也有同样的生命周期问题,但由于某种原因,编译器选择不抱怨那个问题。这对您来说是未定义的行为,无法保证您会收到警告。解决此问题的最快方法是仅按值捕获参数。您可以通过使用命名捕获保留完美的转发属性,但语法有些特殊:

auto x = [...p_argn = std::forward<decltype(p_argn)>(p_argn)]( lambda_t& pp_lambda ) mutable -> decltype(auto) {
    return pp_lambda(std::move(p_argn)... );
};

请确保您了解在这种情况下实际存储的内容,甚至可以绘制图片。在编写这样的代码时,能够准确地知道各个对象所在的位置至关重要,否则很容易编写这样的终身错误。

【讨论】:

    【解决方案2】:

    为什么 GCC 会删除我在 O3 上的代码

    因为 GCC 非常聪明,它会发现您的程序不依赖于任何运行时输入,从而在编译时将其优化为恒定输出。

    只是无法弄清楚到底出了什么问题,以及是什么导致了两个编译器之间的差异。

    程序的行为未定义。没有理由期望编译器之间不存在差异或任何特定行为。

    程序的行为未定义。

    但是为什么呢?

    这里:

    auto xx = Factory(the_lambda, 10, 3);
    

    您将文字传递给函数,它们是纯右值。

    auto Factory( lambda_t&& p_lambda, Tn&& ... p_argn )
    

    函数通过引用接受它们。因此会创建临时对象,其生命周期会一直持续到完整表达式的末尾(比参数引用的生命周期长,因此不会延长临时对象的生命周期)。

    auto x = [&p_argn...]( //...
    

    引用的临时对象存储在 lambda 中...通过引用。绝对不会有整数存储在 lambda 中。

    当您稍后调用 lambda 时,那些被引用的临时对象不再存在。那些不存在的对象在它们的生命周期之外被访问,并且程序的行为是未定义的。


    这样的错误是std::threadstd::bind 和类似的绑定参数总是存储值而不是引用的原因。

    【讨论】:

      【解决方案3】:

      ... 会产生预期的结果,但是打开任何优化级别都会导致我的所有代码被删除。

      问题是:

      你到底期待什么?

      大多数人并不期望程序包含某些汇编代码;大多数人只期望可执行程序(在 Windows 下这将是 .exe 文件)具有某种“黑盒”行为:

      程序应该将某些文本打印到控制台、写入某些文件、在 GUI 中显示某些窗口、在打印机上打印某些文本、创建某些网络连接等等。

      您的程序唯一的“黑盒”行为是它返回退出代码 0。

      这意味着最好的编译器优化可能会丢弃不需要将 0 作为exit() 代码返回的所有内容。

      ...这意味着以下代码保留在 32 位和 64 位 x86 系统上:

      xor eax, eax
      ret
      

      这正是in the link you provided所做的。

      编辑

      抱歉,我没有阅读您问题的以下部分:

      我知道这会导致未定义的行为......

      在这种情况下,这意味着:

      优化的程序 (-O0) 将根据程序启动前 RAM 中的数据返回不同的值。

      根据您使用的操作系统,这可能取决于在您的程序之前运行的程序。

      显然,您的(未优化的)程序的“黑盒”行为可能会返回 0 或 13 作为 exit() 代码,具体取决于启动程序之前 RAM 的内容。

      因此,“最好的”编译器优化可以简单地返回 0 或 13 作为 exit() 代码,假设 RAM 在启动程序之前包含某些数据。

      您可能会争辩:“但是我的操作系统会在程序启动之前将 RAM 内容设置为某个值(例如 0)。”

      但是,即使在这种情况下,exit() 代码仍然取决于(非优化)编译器确切如何翻译程序的方式。

      【讨论】:

        【解决方案4】:

        你从编译器那里得到了一些重要的提示:

        <source>: In function 'int main()':
        
        <source>:45:18: warning: '<anonymous>' is used uninitialized in this function [-Wuninitialized]
        
           45 |         return a+b;
        
              |                  ^
        
        <source>:45:18: warning: '<anonymous>' is used uninitialized in this function [-Wuninitialized]
        
        ASM generation compiler returned: 0
        

        问题是您通过引用捕获了参数列表 (10, 3),但这些是捕获时的临时值。如果您按值捕获或传递实际变量,则代码编译时不会出错,我会得到预期的结果。

        您的所有代码都被“删除”的原因是 gcc 和 clang 都足够聪明,可以意识到您要求它将两个数字相加,因此它们几乎优化了您的整个程序。 finally 程序集如下所示:

        main:
                mov     eax, 13
                ret
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-03-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-01-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多