繁体   English   中英

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

[英]What is aliasing and how does it affect performance?

在GoingNative活动中,在第2天的交互式面板中 ,在9分钟时,Chandler Carruth说:

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

这是什么意思? 这可以用(简单)示例来说明吗?

别名通过阻止编译器进行某些优化来影响性能。 例如:

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的在线演示来获取生成的代码,以下是不同的结果:

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 )中执行更多工作。

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

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

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

这里的别名问题是src可以对dest进行别名,并且生成的代码必须比没有src不允许别名dest效率低。

真正的strcpy使用额外的关键字Restrict技术上只是C的一部分,而不是C ++ ,它告诉编译器假设srcdest不重叠,这允许编译器生成更高效的代码。


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

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语句实际上是有意义的,而不仅仅是if (*a) { *b=*a; *c=*a; } if (*a) { *b=*a; *c=*a; } if (*a) { *b=*a; *c=*a; } ,但目的是一样的。

在写这个时我们可能会假设a != b因为有一些原因使my_function无意义。 但是,编译器不能假设,并执行的存储b和一个重新加载a来自存储器执行所述第二线,其中,以覆盖壳体之前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来消除别名的可能性,编译器会生成更短更快的代码:

0000000000400570 <my_function_2>:
  400570:       8b 07                   mov    (%rdi),%eax
  400572:       85 c0                   test   %eax,%eax
  400574:       74 04                   je     40057a <_Z9my_function_2PiS_S_+0xa>
  400576:       89 06                   mov    %eax,(%rsi)
  400578:       89 02                   mov    %eax,(%rdx)
  40057a:       f3 c3                   repz retq

考虑以下功能:

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]会修改它们的值。 实际上,编译器无法将任何逻辑提升出循环。 因此编译器无法执行“明显的”优化,因为如果参数别名逻辑现在不正确。 但是,如果浮点数是按值获取的,则编译器知道它们不能使用别名并且可以执行提升。

当然,这个功能非常愚蠢,但它证明了这一点。

指针是表示内存地址的值,有时2个指针可以表示与别名相同的内存地址

int * p;
*p = 5;

int * alias;
alias = p;

变量alias是p的别名,如果更改*alias*alias等于5,那么*p随之变化

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM