init-on-first-use函数的gcc属性

时间:2011-07-29 01:26:51

标签: c gcc attributes lazy-initialization

我一直在使用gcc constpure属性来返回指向第一次使用时分配和初始化的“常量”数据的指针的函数,即函数将返回相同的函数每次调用它的价值。作为一个例子(不是我的用例,但是一个众所周知的例子)想到一个函数,它在第一次调用时分配和计算trig查找表,并在第一次调用后返回指向现有表的指针。

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

1 个答案:

答案 0 :(得分:7)

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

“副作用”必须根据C编程语言的语义进行定义,但我们可以根据这些属性的目的来猜测GCC人员的意思,即实现其他优化(至少,这就是我认为的目的。)

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

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

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

变为:

z = y = f(x);

如果zy未使用,则完全被淘汰。

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

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解决此问题。

我认为这个bug不会出现在x86上,我不认为它出现在许多没有多个物理处理器(不是核心)的系统上。所以它可以很好地工作多年,然后在双插槽POWER电脑上突然失败。

结论:这些定义的优点在于它们清楚地说明了在存在这些属性的情况下允许编译器进行哪些更改,我认为这些更改在某种程度上是模糊的。海湾合作委员会文件。缺点是不清楚这些是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:请仔细阅读,因为如果您不知道您对编译器做出的承诺,编译器的未来版本可能会让您大吃一惊。