什么是别名以及它如何影响性能?

时间:2012-03-14 19:55:33

标签: c++

在GoingNative比赛中,在第9天的第2天Interactive Panel期间,Chandler Carruth说:

  

指针会产生锯齿问题。他们放慢你的二进制文件速度而不加速它们。

这是什么意思?这可以用一个(简单的)例子来说明吗?

4 个答案:

答案 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_2LBB0_3之间)。

答案 1 :(得分:12)

Chandler谈论的问题类型可以通过简化的strcpy轻松说明:

char *stpcpy (char * dest, const char * src);

在编写此实现时,您可能会认为dest指向的内存与src指向的内存完全分开。编译器可能希望通过从src指向的字符串中读取一个字符块来优化它,并将所有字符一次写入dest。但如果dest指向src之前的一个字节,则此行为与简单的逐字符副本不同。

这里的别名问题是src可以别名dest,并且生成的代码必须低于src不允许别名{{1}时的效率}}

真正的dest使用额外的关键字Restricttechnically only part of C, not C++,告诉编译器假设strcpysrc不重叠,这允许编译器生成更高效的代码。


这是一个更简单的例子,我们可以看到装配中的一个很大的不同:

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会随之更改