【问题标题】:C++ compiler optimization of passed arguments传递参数的 C++ 编译器优化
【发布时间】:2010-09-21 21:48:03
【问题描述】:

我正在使用可以在运行时启用/禁用报告的日志记录模块。调用通常是这样的:

WARN(
     "Danger Will Robinson! There are "
     + boost::lexical_cast<string>(minutes)
     + " minutes of oxygen left!"
);

我正在为 WARN 使用内联函数,但我很好奇在幕后进行了多少优化 - 评估整个程序中的参数会很昂贵。 WARN 函数是这样的:

bool WARNINGS_ENABLED = false;
inline void WARN(const string &message) {
    if (!WARNINGS_ENABLED) {
       return;
    }
    // ...
}

鉴于构造字符串参数没有副作用,编译器会优化它吗?是否需要一定程度的优化(g++ 中的 -Ox 用于某些 x)?

【问题讨论】:

    标签: c++ optimization compiler-construction arguments


    【解决方案1】:

    我猜它只有在可以证明没有副作用的情况下才有机会对其进行优化(对于昂贵的函数调用,编译器可能很难做到这一点)。

    我不是 boost 专家,但我猜有一种方法可以构造一个 lambda,只有在 WARNINGS_ENABLED 为真时才会对它进行评估以生成字符串。比如……

    inline void warnFunc(some_boost_lambda &message_generator) {
      if (WARNINGS_ENABLED) {
        cerr << message_generator() << endl;
      }
    }
    
    #define WARN(msg) warnFunc(...insert boost magic here to turn msg into a lambda...)
    

    【讨论】:

    • 是的,你的想法很有效。刚刚用 boost::lambda 对其进行了测试(也没有专家,但它很简单)
    • 是的,你的想法很有效。刚刚用 boost::lambda 测试了它(也没有专家,但它是直截了当的)。这是带有输出的示例:codepad.org/PmUh7AHj
    • 不错的工作 litb - 但这还不是一个完整的解决方案(还),因为您尚未证明何时评估 lambda,并且您没有向用户隐藏 lambda。
    • 正确。太糟糕了。另一个 c++1x lambda 将发挥作用的地方:)
    • #define LOGPARAM(...) []() -> 字符串 { 返回 VA_ARGS; } \n 模板 void log(T t) { if(WARNINGS_ENABLED) cerr (42))); ... \n yay 在 gcc 的 lambda 分支上工作 :)
    【解决方案2】:

    您可以使用 -S 选项检查 GCC/G++ 的功能。这将在实际组装之前输出代码 - 请参阅gcc(1)

    在这种情况下,GCC 和 G++ 的行为或多或少是相同的。 所以我先把代码翻译成C来做进一步的测试:

    char WARNINGS_ENABLED = 0;
    
    inline void WARN(const char* message) {
        if (!WARNINGS_ENABLED) {
            return;
        }
        puts(message);
    }
    
    int main() {
        WARN("foo");
        return 0;
    }
    

    运行 gcc -O3 -S file.c 并查看输出文件 'file.s'
    您会看到 GCC 没有删除任何内容

    这不是您要求的,但为了让编译器有机会优化该代码,您必须使 WARNINGS_ENABLED 常量。另一种方法是将其设为 static 并且不更改该文件中的值。 但是:将其设为静态会产生符号无法导出的副作用。

    static const char WARNINGS_ENABLED = 0;
    
    inline void WARN(const char* message) {
      if (!WARNINGS_ENABLED) {
          return;
      }
      puts(message);
    }
    
    int main() {
        WARN("foo");
        return 0;
    }
    

    GCC 然后彻底清理代码。

    【讨论】:

    • 非常好的答案!不幸的是,日志记录行为必须能够在运行时被操纵。
    【解决方案3】:

    你不能用预处理器定义整个事情吗?

    void inline void LogWarning(const string &message) 
    {
      //Warning
    }
    
    #ifdef WARNINGS_ENABLED
    #define WARN(a) LogWarning(a)
    #else
    #define WARN(a)
    #endif
    

    这就是 ASSERT() 宏的工作原理。 WARN 中括号内的所有代码甚至都没有通过预处理器到达编译器。这意味着您可以做其他事情,例如

    #ifdef WARNINGS_ENABLED
    // Extra setup for warning
    #endif
    //....
    WARN(uses setup variables)
    

    它会以两种方式编译。

    至于让优化器意识到括号中没有副作用,您可以在其中放置一些很难证明的非常复杂的语句(即高级字符串操作)。

    【讨论】:

    • 它必须能够运行时切换,但这肯定是一个可行的编译时解决方案。
    • 所以你的问题是:给定一个函数 X(params){if (b){quit};...} 编译器会延迟计算参数直到 if 之后吗?那将是令人印象深刻的。由于分支是动态的,编译器不太可能优化它。
    • 是的,这就是我要问的!除了编译器可以摆脱调用并内联... 中的内容。就像其他人指出的那样,编译器需要能够证明在传递参数时没有外部副作用,这似乎相当困难。
    【解决方案4】:

    如果您需要能够在运行时选择性地启用和禁用警告,编译器将无法优化调用。

    您需要将您的 函数 重命名为 WARN2 并添加如下宏:

    #define WARN(s) do {if (WARNINGS_ENABLED) WARN2(s);} while (false)
    

    这将阻止在运行时对 s 进行评估,除非您启用了警告。

    do-while 是一种技巧,它允许在代码中的任何地方使用它(裸语句、带括号的 if 块中的语句、不带括号的 if 块中的语句、带括号和不带括号的 while 语句等) .

    【讨论】:

    • 是的——很明显,需要在内联函数上使用宏。
    • +1 但这应该是:#define WARN(s) do { if (WARNINGS_ENABLED) WARN2(s); } 而(假)
    • @Tom,你是对的,它不会,但你读过提问者的代码吗?它说“内联无效”。
    • @Greg,谢谢你,我试图记住确保代码在带括号/不带括号的 if/while/do 语句内部/外部工作的技巧。纳入答案。
    • 它是那些“显而易见”的解决方案之一——这就是它的出色之处。我只是想找出其通用应用程序中的缺陷。
    【解决方案5】:

    不,编译器应该在任何情况下优化代码,除非全局 WARNING_ENABLED 声明为 const。

    顺便说一句,如果 WARN 是内联函数,您仍然会为消息构造付出代价(在您的示例中使用 lexical_cast 和 operator+ 字符串时效率非常低下),即使它被禁用。

    以下是一些高效(在禁用运行时时开销最小(使用分支预测 CPU 时接近于零))logging macros,它们支持函数和流式日志记录。

    【讨论】:

      猜你喜欢
      • 2017-11-11
      • 2012-12-16
      • 2011-01-03
      • 1970-01-01
      • 2014-02-21
      • 1970-01-01
      • 1970-01-01
      • 2012-02-09
      相关资源
      最近更新 更多