我一直在使用 gcc const
和 pure
函数的属性,这些函数返回一个指向“常量”数据的指针,该指针在第一次使用时分配和初始化,即函数每次被调用时将返回相同的值。作为一个例子(不是我的用例,而是一个众所周知的例子)考虑一个函数,它在第一次调用时分配和计算触发查找表,并在第一次调用后返回一个指向现有表的指针。
问题:我被告知这种用法是不正确的,因为这些属性禁止副作用,并且如果不使用返回值,编译器甚至可以在某些情况下完全优化调用。我的用法是 const
/pure
属性安全,或者有没有其他方法告诉编译器N>1
对函数的调用相当于对函数的 1 次调用,但对函数的 1 次调用不等于对函数的 0 次调用?或者换句话说,该函数只有在第一次被调用时才有副作用?
最佳答案
我说这是基于我对 pure 和 const 的理解是正确的,但是如果有人对两者有准确的定义,请说出来。这变得很棘手,因为 GCC 文档没有明确说明函数“除了返回值之外没有任何影响”(对于纯)或“不检查除参数之外的任何值”(对于 const)意味着什么。显然所有函数都有一些影响(它们使用处理器周期、修改内存)并检查一些值(函数代码、常量)。
“副作用”必须根据 C 编程语言的语义来定义,但是我们可以根据这些属性的目的来猜测 GCC 人员的意思,即启用额外的优化(至少,这就是我假设他们是为了)。
如果以下某些内容太基本,请原谅我...
纯函数 可以参与公共(public)子表达式消除。它们的特点是不修改环境,因此编译器可以在不改变程序语义的情况下自由调用更少的次数。
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...
常量函数 可以重新排序,因为它们不依赖于动态环境。
// 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))
跨另一个函数调用的函数调用,但似乎完全有可能在 future 发生类似的事情。还记得什么时候-fstrict-aliasing
成为默认设置,然后每个人的程序中突然出现了更多错误?这样的事情让我很谨慎。在我看来,当你标记一个函数时
__attribute__((const))
,您向编译器 promise ,只要参数相同,无论在程序执行期间何时调用该函数调用的结果都是相同的。但是,我确实想出了一种将 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:仔细阅读,因为如果您不知道您对编译器做出了什么 promise ,编译器的 future 版本可能会让您大吃一惊。
关于c - init-on-first-use 函数的 gcc 属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6867696/