C++编译器对传递参数的优化

9
我正在使用一个日志模块,可以在运行时启用/禁用报告功能。通常的调用大致如下:
WARN(
     "Danger Will Robinson! There are "
     + boost::lexical_cast<string>(minutes)
     + " minutes of oxygen left!"
);

我在使用内联函数来进行警告,但是我很好奇后台有多少优化——在整个程序中评估参数将会非常昂贵。 WARN 函数大致如下:

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

考虑到构造字符串参数没有副作用,编译器是否会将其优化掉?是否需要一定程度的优化(例如在某些情况下需要使用-Ox参数)?

5个回答

12

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

您需要做的是将函数重命名为WARN2并添加类似于以下的宏:

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

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

do-while的技巧使它可以在代码的任何位置使用(裸语句、带括号if块内语句、不带括号if块内语句、带括号和不带括号while语句等)。


是的 - 这是一个需要使用宏而不是内联函数的情况。 - cdleary
+1 但这应该是:#define WARN(s) do { if (WARNINGS_ENABLED) WARN2(s); } while(false) - Greg Rogers
@Tom,你说得没错,这确实不会有问题,但是你看过提问者的代码吗?它写着“inline void”。 - paxdiablo
@Greg,谢谢你,我一直在尝试记住确保代码在有/无括号的if/while/do语句内外正常运行的技巧。已经融入到答案中了。 - paxdiablo
这是一个“显而易见”的解决方案,这正是它的精髓所在。我只是试图找出它的通用应用中存在的缺陷。 - Tom Leys
显示剩余4条评论

6
你可以使用-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 const char WARNINGS_ENABLED = 0;

inline void WARN(const char* message) {
  if (!WARNINGS_ENABLED) {
      return;
  }
  puts(message);
}

int main() {
    WARN("foo");
    return 0;
}

GCC 然后完全清理代码。

非常好的答案!不幸的是,记录行为必须能够在运行时进行操作。 - cdleary

1

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

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

这里有一些高效的(最小化(接近于零的分支预测CPU)开销当运行时禁用)日志宏,支持函数和流样式日志记录。


1

我猜只有在编译器能够证明没有副作用时(对于一个昂贵的函数调用来说可能很困难),它才有机会将其优化掉。

我不是boost专家,但我猜应该有一种方法构造一个lambda表达式,只有在WARNINGS_ENABLED为true时才会被评估生成字符串。类似于...

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测试了一下(虽然我也不是专家,但很简单)。 - Johannes Schaub - litb
是的,你的想法很好。我用boost::lambda测试了一下(我也不是专家,但很简单)。这是带有输出的示例:http://codepad.org/PmUh7AHj - Johannes Schaub - litb
干得好,litb - 但这还不是一个完整的解决方案(尚未),因为您尚未证明lambda何时被评估,并且您尚未将lambda隐藏起来使用户无法看到。 - Tom Leys
太糟糕了。又是一个 C++1x Lambda 函数会很棒的地方 :) - Johannes Schaub - litb
#define LOGPARAM(...) -> string { return VA_ARGS; } \n template<typename T> void log(T t) { if(WARNINGS_ENABLED) cerr << t(); } \n ... log(LOGPARAM("Danger: " + lexical_cast<string>(42))); ... \n 太好了,在gcc的lambda分支上运行正常:) - Johannes Schaub - litb

0

你不能只使用预处理器来定义整个东西吗?

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)

而且它可以双向编译。

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


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接