在C++中重新声明变量是否会产生任何费用

Does redeclaring variables in C++ cost anything?

本文关键字:是否 变量 任何费 声明 C++ 新声明      更新时间:2023-10-16

为了可读性,我认为下面的第一个代码块更好。 但是第二个代码块更快吗?

第一个区块:

for (int i = 0; i < 5000; i++){
    int number = rand() % 10000 + 1;
    string fizzBuzz = GetStringFromFizzBuzzLogic(number);
}

第二块:

int number;
string fizzBuzz;
for (int i = 0; i < 5000; i++){
    number = rand() % 10000 + 1;
    fizzBuzz = GetStringFromFizzBuzzLogic(number);
}

在C++中重新声明变量需要任何费用吗?

任何现代编译器都会注意到这一点并进行优化工作。如有疑问,请始终追求可读性。尽可能在最内层范围内声明变量。

我对这个特定的代码进行了基准测试,即使没有优化,两个变体的运行时也几乎相同。一旦启用最低级别的优化,结果就会非常接近相同(+/- 时间测量中的一点噪声(。

编辑:下面对生成的汇编代码的分析表明,很难猜测哪种形式更快,因为大多数人可能会给出的答案是func2,但事实证明这个函数有点慢,至少在使用 clang++ 和 -O2 编译时。这很好地证明了"令状代码,基准测试,更改代码,基准测试"是处理性能的正确方法,而不是基于阅读代码进行猜测。请记住有人告诉我的,优化有点像将洋葱分层拆开 - 一旦你优化了一个部分,你最终会看到非常相似的东西,只是小一点...... ;)

然而,我最初的分析使func1速度明显变慢 - 事实证明,由于某种奇怪的原因,编译器不会在func1中优化rand() % 10000 + 1,而是在优化开启时func2优化。这意味着func1.但是,一旦启用优化,两个函数都会获得"快速"模数。

使用

linux 性能工具perf表明,使用 clang++ 和 -O2,我们可以得到以下 func1

  15.76%  a.out    libc-2.20.so         free
  12.31%  a.out    libstdc++.so.6.0.20  std::string::_S_construct<char cons
  12.29%  a.out    libc-2.20.so         _int_malloc
  10.05%  a.out    a.out                func1
   7.26%  a.out    libc-2.20.so         __random
   6.36%  a.out    libc-2.20.so         malloc
   5.46%  a.out    libc-2.20.so         __random_r
   5.01%  a.out    libstdc++.so.6.0.20  std::basic_string<char, std::char_t
   4.83%  a.out    libstdc++.so.6.0.20  std::string::_Rep::_S_create
   4.01%  a.out    libc-2.20.so         strlen

对于 func2:

  17.88%  a.out    libc-2.20.so         free
  10.73%  a.out    libc-2.20.so         _int_malloc                    
   9.77%  a.out    libc-2.20.so         malloc
   9.03%  a.out    a.out                func2                        
   7.63%  a.out    libstdc++.so.6.0.20  std::string::_S_construct<char con
   6.96%  a.out    libstdc++.so.6.0.20  std::string::_Rep::_S_create
   4.48%  a.out    libc-2.20.so         __random  
   4.39%  a.out    libc-2.20.so         __random_r
   4.10%  a.out    libc-2.20.so         strlen 

存在一些细微的差异,但我认为这些差异更多地与基准测试相对较短的运行时间有关,而不是编译器生成的实际代码的差异。

这是使用以下代码:

#include <iostream>
#include <string>
#include <cstdlib>
#define N 500000
extern std::string GetStringFromFizzBuzzLogic(int number);
void func1()
{
    for (int i = 0; i < N; i++){
        int number = rand() % 10000 + 1;
        std::string fizzBuzz = GetStringFromFizzBuzzLogic(number);
    }
}
void func2()
{
    int number;
    std::string fizzBuzz;
    for (int i = 0; i < N; i++){
        number = rand() % 10000 + 1;
        fizzBuzz = GetStringFromFizzBuzzLogic(number);
    }
}
static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}
int main(int argc, char **argv)
{
    void (*f)();
    if (argc == 1)
    f = func1;
    else
    f = func2;
    for(int i = 0; i < 5; i++)
    {
        unsigned long long t1 = rdtsc();
        f();
        t1 = rdtsc() - t1;
        std::cout << "time=" << t1 << std::endl;
    }
}

并在单独的文件中:

#include <string>
std::string GetStringFromFizzBuzzLogic(int number)
{
    return "SomeString";
}

使用 func1 运行:

./a.out
time=876016390
time=824149942
time=826812600
time=825266315
time=826151399

使用 func2 运行:

./a.out
time=905721532
time=895393507
time=886537634
time=879836476
time=883887384

这是在 N 上又加了 0 - 所以运行时间长了 10 倍 - 似乎它相当一致地慢一点,但它是百分之几,而且可能在噪音范围内,真的 - 随着时间的推移,整个基准测试大约需要 1.30-1.39 秒。

编辑:查看实际循环的汇编代码[这只是循环的一部分,但其余部分在代码实际作用方面是相同的]

函1:

.LBB0_1:                                # %for.body
    callq   rand
    movslq  %eax, %rcx
    imulq   $1759218605, %rcx, %rcx # imm = 0x68DB8BAD
    movq    %rcx, %rdx
    shrq    $63, %rdx
    sarq    $44, %rcx
    addl    %edx, %ecx
    imull   $10000, %ecx, %ecx      # imm = 0x2710
    negl    %ecx
    leal    1(%rax,%rcx), %esi
    movq    %r15, %rdi
    callq   _Z26GetStringFromFizzBuzzLogici
    movq    (%rsp), %rax
    leaq    -24(%rax), %rdi
    cmpq    %rbx, %rdi
    jne .LBB0_2
.LBB0_7:                                # %_ZNSsD2Ev.exit
    decl    %ebp
    jne .LBB0_1

Func2:

.LBB1_1:
    callq   rand
    movslq  %eax, %rcx
    imulq   $1759218605, %rcx, %rcx # imm = 0x68DB8BAD
    movq    %rcx, %rdx
    shrq    $63, %rdx
    sarq    $44, %rcx
    addl    %edx, %ecx
    imull   $10000, %ecx, %ecx      # imm = 0x2710
    negl    %ecx
    leal    1(%rax,%rcx), %esi
    movq    %rbx, %rdi
    callq   _Z26GetStringFromFizzBuzzLogici
    movq    %r14, %rdi
    movq    %rbx, %rsi
    callq   _ZNSs4swapERSs
    movq    (%rsp), %rax
    leaq    -24(%rax), %rdi
    cmpq    %r12, %rdi
    jne .LBB1_4
.LBB1_9:                                # %_ZNSsD2Ev.exit19
    incl    %ebp
    cmpl    $5000000, %ebp          # imm = 0x4C4B40

因此,可以看出,func2版本包含一个额外的函数调用:

    callq   _ZNSs4swapERSs

翻译成std::basic_string<char, std::char_traits<char>, std::allocator<char> >::swap(std::basic_string<char, std::char_traits<char>, std::allocator<char> >&)std::string::swap(std::string&) - 这可能是调用std::string::operator=(std::string &s)的结果。这可以解释为什么func2func1稍慢。

我敢肯定,有可能找到在循环中构造/销毁对象需要大量时间的情况,但总的来说,它几乎没有或根本没有区别,拥有更清晰的代码实际上会帮助读者。它通常也会帮助编译器进行"生命周期分析",因为"走动"以找出稍后是否使用变量的代码更少(在这种情况下,代码无论如何都很短,但在现实生活中的例子中显然并不总是如此(

应该考虑更快的第一个代码块,因为一次调用std::string默认构造函数没有任何开销。

实际上,您没有在第二个代码块中重新声明变量。这些只是普通的赋值操作。

重新声明实际上意味着你有这样的东西

int number;
string fizzBuzz;
for (int i = 0; i < 5000; i++){
    int number = rand() % 10000 + 1;
 // ^^^
    string fizzBuzz = GetStringFromFizzBuzzLogic(number);
 // ^^^^^^
}

在这种情况下,编译器将优化开销,因为根本不使用外部作用域变量。

C++中没有重新声明这样的事情。在第二个代码段中,numberfizzBuzz仅声明和初始化一次。后面的=作业

与所有优化问题一样,您只能猜测或最好进行衡量。当然,这一切都完全取决于您的编译器和您调用它的设置。当然,可以在速度优化和空间优化之间进行权衡。

我知道没有一个严肃的C++程序员不喜欢第一种形式,因为它更容易阅读,也更简洁。

只有程序被认为太慢并且测量代码的哪些部分导致速度变慢并且如果这些测量指向此循环时,他们才会考虑更改它。

然而,正如其他人所说,这是一个不现实的情况。现代编译器极不可能在优化方面以不同的方式处理这两个代码片段,并且您会遇到任何可测量的速度差异。

(编辑:对不起错字,在那里混淆了"第一"和"第二"(

所有声明(值(变量所做的就是将堆栈按该函数/方法中所有局部变量的组合大小递增。

使用对象类型(字符串(调用构造函数/析构函数可能会超过最佳次数,从而产生成本。

在这种情况下没有区别。如果使用体面的编译器,优化器无论如何都会为您提供最佳解决方案。

您可能希望以最佳方式读取代码,这样您的同行就不会认为您编写了糟糕的代码!