变量引用(别名)会产生运行时开销吗?

15
也许这是一个编译器特定的问题。如果是这样,那么对于gcc(g ++)呢?如果您像这样使用变量引用/别名:
int x = 5;
int& y = x;
y += 10;

如果我们不使用引用,它实际上需要更多的周期吗?

int x = 5;
x += 10;

换句话说,机器码是否会改变,还是“别名”只在编译器级别发生?

这可能看起来像一个愚蠢的问题,但我很好奇。特别是在某些情况下,也许临时重命名一些成员变量只是为了让数学代码更容易阅读。当然,我们不是在讨论瓶颈...但这是我正在做的事情,所以我只是想知道是否有任何'真正'的区别...还是只是表面上的差异。

6个回答

11

它可以被视为别名,但不是在效率上。在底层,引用是具有更好的语法和更高安全保证的指针。因此,你会有一个“解引用”操作的运行时惩罚,除非编译器进行了优化,但通常不能依靠这一点。

如果问题是编译器是否会进行优化,那么除了查看生成的汇编代码之外,没有其他方法。


4
答案不完全准确。引用背后的一个意图之一是实现别名的概念,即为现有对象提供另一种名称。我认为这在TC ++ PL中明确说明了。虽然并非总是如此,但“别名”仍然是许多情况下描述引用的准确描述。 - AnT stands with Russia
@AndreyT,我从未听说过引用是别名的想法,你能告诉我标准中哪个段落暗示了这一点吗? - Kornel Kisielewicz
1
@Kornel Kisielewicz:Stroustrup。TC++PL。5.5。“引用是对象的另一个名称”。 - AnT stands with Russia
Kornel:@Andrey明确表示他并没有参考标准。如果你想要严谨一些:§8.3.2.1:注意:引用可以被视为对象的名称。也称为别名。关键是,引用具有旨在表示别名的行为,并不是指针所规定的。 - GManNickG
1
更高的安全保障?这是一个合同问题,没有任何防止您拥有空引用或悬挂引用的东西,唯一与指针不同的是您无法重新设置它。 - Matthieu M.
这个答案可能是肯定的,也可能是否定的。它并没有回答问题。 - Hello World

5

确实,在大多数情况下,引用实现了“别名”这一概念,即对象的另一个名称。

但是,通常情况下,引用是通过指针实现的。然而,一个好的编译器只有在实际绑定是在运行时确定时才会使用实际指针来实现引用。如果绑定在编译时已知(并且类型匹配),编译器通常会将引用实现为相同对象的另一个名称,这种情况下通过引用访问对象与通过原始名称访问对象相比没有性能损失。

您的示例就是其中之一,您不应该期望从引用中获得性能损失。


4

这两个函数在 g++ 中编译出的代码完全相同,即使只使用 -O1。 (我添加了 return 语句以确保计算未被完全消除。)

没有指针,只有引用。在这个简单的例子中,没有性能差异。但不能保证对于所有引用的使用都没有性能差异。

int f()
{
    int x = 5;
    x += 10;
    return x;
}

.

int f()
{
    int x = 5;
    int & y = x;
    y += 10;
    return y;
}

汇编语言:

movl    $15, %eax
ret

1
不确定这是一个公平的测试,因为你的结果显示,整个函数体可以在编译时评估。 - Daniel Pryden
1
@Daniel Pryden:但那就是被问到的代码。你能提出一个更公正的测试吗? - CB Bailey
我认为我的测试更加公平,因为编译器没有在编译时评估函数体。(但我没有测试相同的代码)。 - Hello World

4
唯一确定的方法是编译它并检查编译器的输出。通常,引用的开销与指针的开销类似,因为指针通常是实现引用的方式。但是,考虑到你所展示的简单情况,我相信该引用将被优化掉。

是的,也许有一天我会进行一些简单的测试,看看它是否被优化掉...但现在,这并不太重要。 - cheshirekow

4

我在Gnu/Linux上比较了两个程序。下面只显示了GCC的输出结果,但是clang的结果得出了相同的结论。

GCC版本:4.9.2

Clang版本:3.4.2

这些程序

1.cpp

#include <stdio.h>
int main()
{
    int x = 3;
    printf("%d\n", x);
    return 0;
}

2.cpp

#include <stdio.h>
int main()
{
    int x = 3;
    int & y = x;
    printf("%d\n", y);
    return 0;
}

测试

尝试 1:没有优化

gcc -S --std=c++11 1.cpp

gcc -S --std=c++11 2.cpp

1.cpp 的汇编代码更短。

尝试 2:开启优化

gcc -S -O2 --std=c++11 1.cpp

gcc -S -O2 --std=c++11 2.cpp

生成的汇编代码完全相同。

汇编输出

1.cpp,无优化

    .file   "1.cpp"
    .section    .rodata
.LC0:
    .string "%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $3, -4(%rbp)
    movl    -4(%rbp), %eax
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.9.2-10) 4.9.2"
    .section    .note.GNU-stack,"",@progbits

2.cpp,无优化

    .file   "2.cpp"
    .section    .rodata
.LC0:
    .string "%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $3, -12(%rbp)
    leaq    -12(%rbp), %rax
    movq    %rax, -8(%rbp)
    movq    -8(%rbp), %rax
    movl    (%rax), %eax
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.9.2-10) 4.9.2"
    .section    .note.GNU-stack,"",@progbits

1. 使用优化后的cpp

    .file   "1.cpp"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "%d\n"
    .section    .text.unlikely,"ax",@progbits
.LCOLDB1:
    .section    .text.startup,"ax",@progbits
.LHOTB1:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB12:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $3, %esi
    movl    $.LC0, %edi
    xorl    %eax, %eax
    call    printf
    xorl    %eax, %eax
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE12:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE1:
    .section    .text.startup
.LHOTE1:
    .ident  "GCC: (Debian 4.9.2-10) 4.9.2"
    .section    .note.GNU-stack,"",@progbits

2.cpp,进行优化

    .file   "1.cpp"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "%d\n"
    .section    .text.unlikely,"ax",@progbits
.LCOLDB1:
    .section    .text.startup,"ax",@progbits
.LHOTB1:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB12:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $3, %esi
    movl    $.LC0, %edi
    xorl    %eax, %eax
    call    printf
    xorl    %eax, %eax
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE12:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE1:
    .section    .text.startup
.LHOTE1:
    .ident  "GCC: (Debian 4.9.2-10) 4.9.2"
    .section    .note.GNU-stack,"",@progbits

结论

在优化后的GCC输出中,不存在运行时成本。同样适用于clang(测试版本为3.4.2):当开启优化时,生成的汇编代码在两个程序中是相同的。


0

是的,解引用引用后面的指针会产生额外的运行时成本,但可能微不足道。以最清晰易读和最清晰地表达你所追求的语义的方式编写代码,如果性能是一个问题,则在分析器中运行(瓶颈很少是你猜测的那样)。如果您使用的是MacOS,则Shark非常棒。


完成。就像我说的,只是好奇。在 Mac 上开发?呃... ;) - cheshirekow
不要轻视你不了解的东西! :-) 调试和分析工具非常出色,而且不需要花费大量金钱。 - metasim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接