__attribute((const))的gcc行为不一致

Inconsistent gcc behaviour for __attribute((const))

本文关键字:不一致 gcc attribute const      更新时间:2023-10-16

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

这并不是一个真正的bug,因为__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; 
    }

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

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似乎都是在编译时预先计算的。

编译器不信任您。由于您有一个引用参数,编译器似乎不信任您的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 %dn", 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