【问题标题】:Inconsistent gcc behaviour for __attribute((const))__attribute((const)) 的 gcc 行为不一致
【发布时间】:2013-12-02 13:28:36
【问题描述】:

我在 gcc 中遇到了一个非常奇怪的行为,涉及标记为 __attribute((const)) 的运算符和函数。逻辑和算术运算符导致不同的优化,我不明白为什么。

这并不是一个真正的错误,因为__attribute((const)) 只是一个提示,并不能保证它的效果,但这仍然是非常令人惊讶的。有人解释一下吗?

这是代码。所以我定义了一个__attribute((const)) 函数:

int f(int & counter) __attribute((const));
int f(int & counter) {
    ++counter;
    return 0;
}

然后我定义一个算子测试宏。这是通过宏而不是模板/仿函数来完成的,以便向编译器提供简单的代码并简化优化:

int global = 0; // forces results to be computed

#define TestOp(OP) \
    { \
        int n = 0; \
        global += (f(n) OP f(n)); \
        std::cout << "op" #OP " calls f " << n << " times" << std::endl; \
    }

最后,我测试不同的运算符如下。 cmets 在-O3-Ofast 处将输出与g++-4.8 -std=c++11 -O2 -Wall -pedantic 相同的输出匹配

int main() {
    // all calls optimized away
    TestOp(^)   // 0
    TestOp(-)   // 0
    // one call is optimized away
    TestOp(|)   // 1
    TestOp(&)   // 1
    TestOp(||)  // 1
    TestOp(&&)  // 1
    // no optimization
    TestOp(+)   // 2
    TestOp(*)   // 2

    return global;
}

我的问题是:为什么算术运算符会产生两个调用?为什么 f()+f() 不能优化为 2*f() ?有没有办法帮助/强制这种优化? 起初我认为乘法可能更昂贵,但我尝试了f()+....+f(),10 次加法仍然没有减少到10*f()。此外,由于它是int 算术,因此操作顺序无关紧要(与floats 相反)。

我还检查了 asm,但没有帮助:所有 int 似乎都是在编译时预先计算的。

【问题讨论】:

  • 你可以试试 O3
  • @PlasmaHH:不会改变
  • 文档警告说 const 函数不能检查指针参数,我认为对于引用来说这将是相同的(更不用说更改传入的任何参数 - const 应该是甚至比纯属性更严格)。
  • @nos 好吧,显然没有“合法”的方式来调试 const 函数,所以我坚持这一点。但这不是问题,因为没有检查这些属性,编译器应该相信我。
  • @Antonie 好吧,如果你创建你的函数 int f(int i) __attribute((const)); ,将实现放在不同的编译单元中(它会改变一个全局变量来跟踪计数),我会得到完全不同的结果 -只有 TestOp(|) 执行 1 次调用,其他都被优化掉,这将是调用 f(0) 时的预期情况。

标签: c++ gcc compiler-optimization function-attributes


【解决方案1】:

编译器不信任你。 由于您有一个引用参数,编译器似乎不信任您的 const 属性 - const 函数应该只查看通过参数传递的值(而不是引用或取消引用指针)。

另一种测试方法是将const 函数分解为单独的编译单元:

test1.cpp:

#include <stdio.h>
int global = 0; // forces results to be computed

int f(int i) __attribute((const));
void print_count(void);

#define TestOp(OP) \
    { \
        int n = 0; \
        global += (f(n) OP f(n)); \
        printf("op %s ", #OP);\
        print_count();\
    }

int main() {
    // all calls optimized away
    TestOp(^)   // 0
    TestOp(-)   // 0
    // one call is optimized away
    TestOp(|)   // 1
    TestOp(&)   // 1
    TestOp(||)  // 1
    TestOp(&&)  // 1
    // no optimization
    TestOp(+)   // 2
    TestOp(*)   // 2

    return global;
}

counter.cpp:

#include <stdio.h>
static int counter = 0;

int f(int i) {
    ++counter;
    return 0;
}

void print_count(void)
{
   printf("counter %d\n", counter);
    counter = 0;
}

现在编译器发现在f(0) | f(0) 之前不需要调用f(0),并且对f(0) 的调用结果会重新用于其他情况。

$ g++ -O2 -c counter.cpp && g++ -O2 -c test.cpp && g++ counter.o test.o && ./a.out
op ^ counter 0
op - counter 0
op | counter 1
op & counter 0
op || counter 0
op && counter 0
op + counter 0
op * counter 0

【讨论】:

  • 谢谢!所以编译器只有在另一个编译单元中才“信任”我?当它在同一个单元中时,当编译器删除我的属性注释时发出警告是否有意义?
  • 但是f(0) | f(0) 是怎么回事?我不明白为什么它应该有任何不同
  • @harold 我改变了它,它并不特别——它只是编译器第一次需要调用 f(0)。为了能够单独测试所有运算符,您必须为每个运算符调用 f(n),并使用不同的 n。
  • 我认为在这种情况下编译器不信任他是件好事。展开解释,它之所以在单独的编译单元中起作用,是因为在调用处,编译器看不到内容(因为它在另一个编译单元中),因此更愿意相信注释,因为它没有理由不这样做。
猜你喜欢
  • 1970-01-01
  • 2020-05-14
  • 1970-01-01
  • 1970-01-01
  • 2011-01-21
  • 1970-01-01
  • 1970-01-01
  • 2014-03-13
  • 2021-04-02
相关资源
最近更新 更多