在GoingNative比赛中,在第9天的第2天Interactive Panel期间,Chandler Carruth说:
指针会产生锯齿问题。他们放慢你的二进制文件速度而不加速它们。
这是什么意思?这可以用一个(简单的)例子来说明吗?
答案 0 :(得分:23)
别名会阻止编译器执行某些优化,从而影响性能。例如:
void foo(int *array,int *size,int *value) {
for(int i=0;i<*size;++i) {
array[i] = 2 * *value;
}
}
查看此代码,您可能希望编译器可以在循环外部加载*value
,然后非常快速地将数组中的每个元素设置为该值。但由于混叠,情况并非如此。因为*value
可以是数组元素的别名,所以它可以在任何给定的迭代中更改。因此,代码必须在每次迭代时加载值,从而导致潜在的大幅减速。
如果变量无法别名,则上述代码将等同于以下内容:
void foo(int *array,int size,int value) {
for(int i=0;i<size;++i) {
array[i] = 2 * value;
}
}
使用LLVM&#39; online demo获取生成的代码,以下是不同的结果:
1)使用别名
foo: # @foo
.cfi_startproc
# BB#0:
cmpl $0, (%rsi)
jle .LBB0_3
# BB#1:
xorl %eax, %eax
.align 16, 0x90
.LBB0_2: # %.lr.ph
# =>This Inner Loop Header: Depth=1
movl (%rdx), %ecx
addl %ecx, %ecx
movl %ecx, (%rdi,%rax,4)
incq %rax
cmpl (%rsi), %eax
jl .LBB0_2
.LBB0_3: # %._crit_edge
ret
.size foo, .Ltmp1-foo
.cfi_endproc
.Leh_func_end0:
2)没有别名
foo: # @foo
.cfi_startproc
# BB#0:
testl %esi, %esi
jle .LBB0_3
# BB#1: # %.lr.ph
addl %edx, %edx
.align 16, 0x90
.LBB0_2: # =>This Inner Loop Header: Depth=1
movl %edx, (%rdi)
addq $4, %rdi
decl %esi
jne .LBB0_2
.LBB0_3: # %._crit_edge
ret
.size foo, .Ltmp1-foo
.cfi_endproc
.Leh_func_end0:
您可以看到带有别名的版本必须在循环体中执行更多工作(标签LBB0_2
和LBB0_3
之间)。
答案 1 :(得分:12)
Chandler谈论的问题类型可以通过简化的strcpy
轻松说明:
char *stpcpy (char * dest, const char * src);
在编写此实现时,您可能会认为dest
指向的内存与src
指向的内存完全分开。编译器可能希望通过从src
指向的字符串中读取一个字符块来优化它,并将所有字符一次写入dest
。但如果dest
指向src
之前的一个字节,则此行为与简单的逐字符副本不同。
这里的别名问题是src
可以别名dest
,并且生成的代码必须低于src
不允许别名{{1}时的效率}}
真正的dest
使用额外的关键字Restrict(technically only part of C, not C++,告诉编译器假设strcpy
和src
不重叠,这允许编译器生成更高效的代码。
这是一个更简单的例子,我们可以看到装配中的一个很大的不同:
dest
假设这是函数的简化,其中使用两个if语句而不仅仅是void my_function_1(int* a, int* b, int* c) {
if (*a) *b = *a;
if (*a) *c = *a;
}
void my_function_2(int* __restrict a, int* __restrict b, int* __restrict c) {
if (*a) *b = *a;
if (*a) *c = *a;
}
实际上是有意义的,但意图是相同的。
我们可能会在写这篇文章时假设if (*a) { *b=*a; *c=*a; }
,因为有一些理由说明a != b
这样使用是没有意义的。但是编译器不能在执行第二行之前假定存储my_function
并从内存中重新加载b
,以涵盖a
:< / p>
b == a
如果我们通过添加0000000000400550 <my_function_1>:
400550: 8b 07 mov (%rdi),%eax
400552: 85 c0 test %eax,%eax <= if (*a)
400554: 74 0a je 400560 <my_function_1+0x10>
400556: 89 06 mov %eax,(%rsi)
400558: 8b 07 mov (%rdi),%eax
40055a: 85 c0 test %eax,%eax <= if (*a)
40055c: 74 02 je 400560 <my_function_1+0x10>
40055e: 89 02 mov %eax,(%rdx)
400560: f3 c3 repz retq
来消除别名的可能性,编译器会生成更短更快的代码:
__restrict
答案 2 :(得分:4)
考虑以下功能:
void f(float* lhs, float* rhs, float* out, int size) {
for(int i = 0; i < size; i++) {
out[i] = *lhs + *rhs;
}
}
这个功能的最快版本是什么?也许,你将*lhs + *rhs
提升出循环。问题是当指针别名时会发生什么。想象一下,如果我这样称呼它,优化会做什么:
float arr[6] = { ... };
f(arr, arr + 1, arr, 6);
正如您所看到的,问题是*lhs + *rhs
无法从循环中提升,因为out[i]
会修改其值。实际上,编译器无法将任何逻辑提升出循环。因此编译器无法执行“明显的”优化,因为如果参数别名逻辑现在不正确。但是,如果浮点值是按值获取的,则编译器知道它们不能使用别名并且可以执行提升。
当然,这个功能非常愚蠢,但它证明了这一点。
答案 3 :(得分:1)
指针是一个表示内存地址的值,有时2个指针可以表示与别名相同的内存地址
int * p;
*p = 5;
int * alias;
alias = p;
变量alias
是p的别名,*alias
等于5,如果您更改*alias
,则*p
会随之更改