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

R..*_*R.. 7 c attributes gcc lazy-initialization

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

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

Die*_*Epp 7

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

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

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

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

z = f(x);
y = f(x);
Run Code Online (Sandbox Code Playgroud)

变为:

z = y = f(x);
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

所以我最好的猜测是"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;
}
Run Code Online (Sandbox Code Playgroud)

由于它是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
Run Code Online (Sandbox Code Playgroud)

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

我不认为这个bug会出现在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;
}
Run Code Online (Sandbox Code Playgroud)

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

int func(int x)
{
    int y;
    y = const_func(x);
    pthread_mutex_lock(&mutex);
    pthread_mutex_unlock(&mutex);
    return y * 2;
}
Run Code Online (Sandbox Code Playgroud)

请注意,这不会仅发生__attribute__((pure)),const属性和仅const属性会触发此行为.

如您所见,临界区内的呼叫消失了.保留早期调用似乎相当武断,我不愿意下注编译器在将来的某个版本中不会做出关于要保留哪个调用的不同决定,或者它是否可能在某个地方调用函数调用完全是.

结论2:仔细阅读,因为如果你不知道你对编译器做了什么承诺,编译器的未来版本可能会让你大吃一惊.

  • @R ..但是如果x,y和z都是非混淆的局部变量,编译器可以自由地在互斥锁上移动`x = y + z`.当你指定一个函数是`__attribute __((const))`时,你告诉编译器该函数没有对非常量内存进行任何加载和存储,因此是否允许编译器在一个函数中移动加载和存储.互斥是无关紧要的. (2认同)