7

我一直在使用函数的 gccconstpure属性,这些函数返回指向在第一次使用时分配和初始化的“常量”数据的指针,即每次调用函数时都会返回相同的值。作为一个例子(不是我的用例,而是一个众所周知的例子)考虑一个函数,它在第一次调用时分配和计算三角查找表,并在第一次调用后返回一个指向现有表的指针。

问题:我被告知这种用法是不正确的,因为这些属性禁止副作用,并且如果不使用返回值,编译器甚至可以在某些情况下完全优化调用。我对const/pure属性的使用是安全的,还是有任何其他方法可以告诉编译器N>1对函数的调用相当于对函数的 1 次调用,但对函数的 1 次调用不等于对函数的 0 次调用?或者换句话说,该函数仅在第一次调用时才有副作用?

4

1 回答 1

7

根据我对pureconst的理解,我说这是正确的,但如果有人对两者有准确的定义,请说出来。这变得很棘手,因为 GCC 文档没有准确说明函数“除了返回值之外没有任何影响”(对于pure)或“不检查除参数之外的任何值”(对于const)意味着什么。显然,所有函数都有一些效果(它们使用处理器周期、修改内存)并检查一些值(函数代码、常量)。

“副作用”必须根据 C 编程语言的语义来定义,但我们可以根据这些属性的目的猜测 GCC 人的意思,即启用额外的优化(至少,这就是我假设他们是为了)。

如果以下某些内容太基本,请原谅我...

纯函数可以参与公共子表达式消除。它们的特点是它们不修改环境,因此编译器可以在不改变程序语义的情况下自由调用它的次数更少。

z = f(x);
y = f(x);

变成:

z = y = f(x);

或者如果z并且y未被使用则完全被淘汰。

所以我最好的猜测是,“纯”的工作定义是“任何可以在不改变程序语义的情况下被更少调用的函数”。但是,函数调用可能不会被移动,例如,

size_t l = strlen(str); // strlen is pure
*some_ptr = '\0';
// Obviously, strlen can't be moved here...

const 函数可以重新排序,因为它们不依赖于动态环境。

// Assuming x and y not aliased, sin can be moved anywhere
*some_ptr = '\0';
double y = sin(x);
*other_ptr = '\0';

所以我最好的猜测是“const”的工作定义是“可以在任何时候调用而不改变程序语义的任何函数”。但是,有一个危险:

__attribute__((const))
double big_math_func(double x, double theta, double iota)
{
    static double table[512];
    static bool initted = false;
    if (!initted) {
        ...
        initted = true;
    }
    ...
    return result;
}

由于它是 const,编译器可以重新排序它......

pthread_mutex_lock(&mutex);
...
z = big_math_func(x, theta, iota);
...
pthread_mutex_unlock(&mutex);
// big_math_func might go here, if the compiler wants to

在这种情况下,它可以从两个处理器同时调用,即使它只出现在代码的关键部分内。然后处理器可以决定将更改推迟table到已经完成的更改之后initted,这是个坏消息。您可以使用内存屏障或pthread_once.

我认为这个错误永远不会出现在 x86 上,而且我认为它不会出现在许多没有多个物理处理器(不是内核)的系统上。所以它可以正常工作很长时间,然后在双插槽 POWER 计算机上突然失败。

结论:这些定义的优点是它们清楚地说明了在存在这些属性的情况下允许编译器进行哪些类型的更改,这(我认为)在 GCC 文档中有些模糊。缺点是不清楚这些是 GCC 团队使用的定义。

例如,如果您查看 Haskell 语言规范,您会发现纯度更精确的定义,因为纯度对 Haskell 语言非常重要。

编辑:我无法强迫 GCC 或 Clang 将一个单独的__attribute__((const))函数调用移动到另一个函数调用中,但似乎完全有可能在未来发生类似的事情。还记得什么时候-fstrict-aliasing成为默认设置,每个人的程序中突然出现了更多的错误吗? 正是这样的事情让我很谨慎。

在我看来,当你标记一个函数时__attribute__((const)),你是在向编译器保证函数调用的结果是相同的,无论在你的程序执行期间何时调用它,只要参数相同。

然而,我确实想出了一种将 const 函数移出临界区的方法,尽管我这样做的方法可以称为某种“作弊”。

__attribute__((const))
extern int const_func(int x);

int func(int x)
{
    int y1, y2;
    y1 = const_func(x);
    pthread_mutex_lock(&mutex);
    y2 = const_func(x);
    pthread_mutex_unlock(&mutex);
    return y1 + y2;
}

编译器将其转换为以下代码(来自程序集):

int func(int x)
{
    int y;
    y = const_func(x);
    pthread_mutex_lock(&mutex);
    pthread_mutex_unlock(&mutex);
    return y * 2;
}

__attribute__((pure))请注意,只有、const属性不会发生这种情况,只有const属性会触发此行为。

如您所见,关键部分内的调用消失了。保留较早的调用似乎相当随意,我不愿意打赌编译器不会在未来的某个版本中做出不同的决定来保留哪个调用,或者它是否可能将函数调用移动到某个地方否则完全。

结论 2:小心行事,因为如果你不知道你对编译器做出了什么承诺,编译器的未来版本可能会让你大吃一惊。

于 2011-07-29T02:52:25.297 回答