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

Sta*_*ked 16 c++

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

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

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

bam*_*s53 25

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

void foo(int *array,int *size,int *value) {
    for(int i=0;i<*size;++i) {
        array[i] = 2 * *value;
    }
}
Run Code Online (Sandbox Code Playgroud)

查看此代码,您可能希望编译器可以*value在循环外部加载一次,然后非常快速地将数组中的每个元素设置为该值.但由于混叠,情况并非如此.因为*value它可能是数组元素的别名,所以它可以在任何给定的迭代中改变.因此,代码必须每次迭代加载值,从而导致潜在的大幅减速.

如果变量不能别名,那么上面的代码将等同于以下内容:

void foo(int *array,int size,int value) {
    for(int i=0;i<size;++i) {
        array[i] = 2 * value;
    }
}
Run Code Online (Sandbox Code Playgroud)

使用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:
Run Code Online (Sandbox Code Playgroud)

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:
Run Code Online (Sandbox Code Playgroud)

您可以看到带有别名的版本必须在循环体(标签LBB0_2和之间LBB0_3)中执行更多工作.

  • @bumfo不,我不希望在这种情况下改进任何东西,因为`const`实际上并不意味着该值不会被改变.它所说的是该函数不会通过该指针改变它.(虽然技术上"常量"甚至没有那么多意思.) (3认同)

je4*_*e4d 12

钱德勒谈论的问题类型可以通过简化来轻松说明strcpy:

char *stpcpy (char * dest, const char * src);
Run Code Online (Sandbox Code Playgroud)

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

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

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


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

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;
}
Run Code Online (Sandbox Code Playgroud)

假设这是一个函数的简化,其中实际使用两个if语句而不仅仅是有意义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
Run Code Online (Sandbox Code Playgroud)

如果我们通过添加来消除混叠的可能性__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
Run Code Online (Sandbox Code Playgroud)