【发布时间】:2019-06-07 10:25:54
【问题描述】:
编辑 1: 添加了另一个示例(表明 GCC 原则上能够完成我想要实现的目标)并在此问题的末尾进行了更多讨论。
编辑2:找到malloc函数属性,应该做什么。请看问题的最后。
这是一个关于如何告诉编译器存储到内存区域的问题在区域之外不可见(因此可以优化掉)。为了说明我的意思,我们来看看下面的代码
int f (int a)
{
int v[2];
v[0] = a;
v[1] = 0;
while (v[0]-- > 0)
v[1] += v[0];
return v[1];
}
gcc -O2 生成以下汇编代码(x86-64 gcc, trunk, on https://godbolt.org):
f:
leal -1(%rdi), %edx
xorl %eax, %eax
testl %edi, %edi
jle .L4
.L3:
addl %edx, %eax
subl $1, %edx
cmpl $-1, %edx
jne .L3
ret
.L4:
ret
可以看到,数组v的加载和存储在优化后都消失了。
现在考虑以下代码:
int g (int a, int *v)
{
v[0] = a;
v[1] = 0;
while (v[0]-- > 0)
v[1] += v[0];
return v[1];
}
不同之处在于v 不是在函数中分配(堆栈),而是作为参数提供。在这种情况下gcc -O2 的结果是:
g:
leal -1(%rdi), %edx
movl $0, 4(%rsi)
xorl %eax, %eax
movl %edx, (%rsi)
testl %edi, %edi
jle .L4
.L3:
addl %edx, %eax
subl $1, %edx
cmpl $-1, %edx
jne .L3
movl %eax, 4(%rsi)
movl $-1, (%rsi)
ret
.L4:
ret
显然,代码必须将v[0] 和v[1] 的最终值存储在内存中,因为它们可能是可观察到的。
现在,我正在寻找一种方法来告诉编译器第二个示例中v 指向的内存在函数g 返回后不再可访问,以便编译器可以优化远离内存访问。
举个更简单的例子:
void h (int *v)
{
v[0] = 0;
}
如果v 指向的内存在h 返回后无法访问,应该可以将函数简化为单个ret。
我试图通过使用严格的别名规则来实现我想要的,但没有成功。
在编辑 1 中添加:
GCC 似乎内置了必要的代码,如下例所示:
include <stdlib.h>
int h (int a)
{
int *v = malloc (2 * sizeof (int));
v[0] = a;
v[1] = 0;
while (v[0]-- > 0)
v[1] += v[0];
return v[1];
}
生成的代码不包含任何加载和存储:
h:
leal -1(%rdi), %edx
xorl %eax, %eax
testl %edi, %edi
jle .L4
.L3:
addl %edx, %eax
subl $1, %edx
cmpl $-1, %edx
jne .L3
ret
.L4:
ret
换句话说,GCC 知道通过malloc 的任何副作用无法观察到更改v 指向的内存区域。对于这样的目的,GCC 有__builtin_malloc。
所以我也可以问:用户代码(比如malloc 的用户版本)如何使用此功能?
在编辑 2 中添加:
GCC具有以下功能属性:
malloc
这告诉编译器一个函数是类似malloc的,即函数返回的指针P不能为函数返回时任何其他有效的指针起别名,而且在P寻址的任何存储中都不会出现指向有效对象的指针.
使用此属性可以改进优化。编译器预测具有该属性的函数在大多数情况下返回非空值。 malloc 和 calloc 之类的函数具有此属性,因为它们返回指向未初始化或清零存储的指针。然而,像 realloc 这样的函数没有这个属性,因为它们可以返回一个指向包含指针的存储的指针。
它似乎做我想做的事,如下例所示:
__attribute__ (( malloc )) int *m (int *h);
int i (int a, int *h)
{
int *v = m (h);
v[0] = a;
v[1] = 0;
while (v[0]-- > 0)
v[1] += v[0];
return v[1];
}
生成的汇编代码没有加载和存储:
i:
pushq %rbx
movl %edi, %ebx
movq %rsi, %rdi
call m
testl %ebx, %ebx
jle .L4
leal -1(%rbx), %edx
xorl %eax, %eax
.L3:
addl %edx, %eax
subl $1, %edx
cmpl $-1, %edx
jne .L3
popq %rbx
ret
.L4:
xorl %eax, %eax
popq %rbx
ret
但是,一旦编译器看到m 的定义,它可能会忘记该属性。例如,当给出以下定义时就是这种情况:
__attribute__ (( malloc )) int *m (int *h)
{
return h;
}
在这种情况下,函数是内联的,编译器会忘记该属性,从而产生与函数 g 相同的代码。
P.S.:最初,我认为restrict 关键字可能会有所帮助,但事实并非如此。
【问题讨论】:
-
嗯,您能否提供一个可能有用的真实案例?如果“外部”不关心缓冲区的状态,您不能只在本地分配它吗?还是因为它是某种用户提供的暂存缓冲区?
-
在实际用例中,缓冲区由带有 GC 的内存管理系统提供。在堆栈上分配是不可能的,比如
alloca,因为我需要 TCO 才能工作并且因为我需要严格限制堆栈空间。 -
如果从另一个模块调用
g()会怎样?它必须遵守声明。如果你把它设为inline,那么它可能会在调用站点进行优化。 -
我的真实世界代码不是手工编写的,而是由转译器生成的。转译器不知道适合局部变量等的最大缓冲区是多少。因此我一直在寻找通用解决方案。
-
我查看了 GCC 扩展(不是详尽无遗,只是可能的类别),没有看到任何可以提供请求功能的东西。理论上,
unsigned char a[1]; memset(v, a[1], length);可以通过将v设置为“不确定”值来实现这一点,这可以让编译器优化知道之前写入v的任何内容随后都不会使用,memset不会使用实际需要。 (虽然它名义上将所有v设置为一个值,但“不确定”允许其他情况。)但我怀疑编译器在这种情况下实际上会按预期运行。
标签: c gcc compiler-optimization