在 C++ 中重新声明变量是否有任何成本?
Does redeclaring variables in C++ cost anything?
为了可读性,我认为下面的第一个代码块更好。但是第二个代码块更快吗?
第一块:
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++ 中重新声明变量是否需要任何费用?
任何现代编译器都会注意到这一点并进行优化工作。
如有疑问,请始终追求可读性。尽可能在最内层的范围内声明变量。
第一个代码块应该被认为更快,因为调用一次 std::string
默认构造函数没有任何开销。
实际上您没有在第二个代码块中重新声明变量。这些只是简单的赋值操作。
重新声明实际上意味着你有这样的东西
int number;
string fizzBuzz;
for (int i = 0; i < 5000; i++){
int number = rand() % 10000 + 1;
// ^^^
string fizzBuzz = GetStringFromFizzBuzzLogic(number);
// ^^^^^^
}
在这种情况下,编译器会优化开销,因为根本不使用外部作用域变量。
所有声明(值)变量所做的是将堆栈增加 function/method 中所有局部变量的组合大小。
使用对象类型(您的字符串)调用构造函数/析构函数的次数可能会超过最佳次数。
在这种情况下没有区别。如果使用像样的编译器,优化器无论如何都会为您提供最佳解决方案。
您可能希望代码以最佳方式阅读,这样您的同行就不会认为您编写了糟糕的代码!
C++ 中没有重新声明这样的东西。在您的第二个代码片段中, number
和 fizzBuzz
仅声明和初始化一次。后面的 =
是 assignments.
与所有优化问题一样,您只能猜测或最好测量。当然,这完全取决于您的编译器和您调用它的设置。当然,可以在速度优化和 space 优化之间进行权衡。
据我所知,没有一个认真的 C++ 程序员不喜欢第一种形式,因为它更容易阅读,而且更简洁。
只有if程序会被认为太慢而if 测量了代码的哪些部分导致速度下降,如果 那些测量指向这个循环,只有这样才会他们考虑改变它。
然而,正如其他人所说,这是一个不现实的场景。现代编译器极不可能在优化方面以不同的方式处理这两个片段,并且您会体验到任何可测量的速度差异。
(编辑:抱歉打错了,混淆了 "first" 和 "second")
我对这个特定代码进行了基准测试,即使没有优化,两个变体的运行时间也几乎相同。一旦打开最低级别的优化,结果就非常接近相同(+/- 时间测量中的一点噪音)。
编辑:下面对生成的汇编代码的分析表明,很难猜测哪种形式更快,因为大多数人可能给出的答案是 func2
,但事实证明这个函数有点小速度较慢,至少在使用 clang++ 和 -O2 进行编译时是这样。这很好地证明 "writ code, benchmark, change code, benchmark" 是处理性能的正确方法,而不是根据阅读代码进行猜测。记住有人告诉我的话,优化有点像把洋葱分成几层——一旦你优化了一个部分,你最终会看到非常相似的东西,只是小了一点……;)
然而,我的初步分析使 func1
显着变慢 - 结果证明是因为编译器出于某种奇怪的原因没有优化 func1
中的 rand() % 10000 + 1
但是在关闭优化时在 func2
中执行。这意味着 func1
。但是,一旦启用优化,两个函数都会获得 "fast" 模。
使用 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 59218605, %rcx, %rcx # imm = 0x68DB8BAD
movq %rcx, %rdx
shrq , %rdx
sarq , %rcx
addl %edx, %ecx
imull 000, %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
函数 2:
.LBB1_1:
callq rand
movslq %eax, %rcx
imulq 59218605, %rcx, %rcx # imm = 0x68DB8BAD
movq %rcx, %rdx
shrq , %rdx
sarq , %rcx
addl %edx, %ecx
imull 000, %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 00000, %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)
的结果。这可以解释为什么 func2
比 func1
稍慢。
我确信可以找到 constructing/destroying 一个对象在一个循环中花费大量时间的情况,但一般来说,它几乎没有或根本没有区别,并且代码更清晰实际上会帮助reader。它也经常帮助编译器使用 "life-time analysis",因为它比 "walk" 更少的代码来查明变量是否在以后使用(在这种情况下,代码很短,但显然并不总是现实生活中的案例)
为了可读性,我认为下面的第一个代码块更好。但是第二个代码块更快吗?
第一块:
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++ 中重新声明变量是否需要任何费用?
任何现代编译器都会注意到这一点并进行优化工作。 如有疑问,请始终追求可读性。尽可能在最内层的范围内声明变量。
第一个代码块应该被认为更快,因为调用一次 std::string
默认构造函数没有任何开销。
实际上您没有在第二个代码块中重新声明变量。这些只是简单的赋值操作。
重新声明实际上意味着你有这样的东西
int number;
string fizzBuzz;
for (int i = 0; i < 5000; i++){
int number = rand() % 10000 + 1;
// ^^^
string fizzBuzz = GetStringFromFizzBuzzLogic(number);
// ^^^^^^
}
在这种情况下,编译器会优化开销,因为根本不使用外部作用域变量。
所有声明(值)变量所做的是将堆栈增加 function/method 中所有局部变量的组合大小。
使用对象类型(您的字符串)调用构造函数/析构函数的次数可能会超过最佳次数。
在这种情况下没有区别。如果使用像样的编译器,优化器无论如何都会为您提供最佳解决方案。
您可能希望代码以最佳方式阅读,这样您的同行就不会认为您编写了糟糕的代码!
C++ 中没有重新声明这样的东西。在您的第二个代码片段中, number
和 fizzBuzz
仅声明和初始化一次。后面的 =
是 assignments.
与所有优化问题一样,您只能猜测或最好测量。当然,这完全取决于您的编译器和您调用它的设置。当然,可以在速度优化和 space 优化之间进行权衡。
据我所知,没有一个认真的 C++ 程序员不喜欢第一种形式,因为它更容易阅读,而且更简洁。
只有if程序会被认为太慢而if 测量了代码的哪些部分导致速度下降,如果 那些测量指向这个循环,只有这样才会他们考虑改变它。
然而,正如其他人所说,这是一个不现实的场景。现代编译器极不可能在优化方面以不同的方式处理这两个片段,并且您会体验到任何可测量的速度差异。
(编辑:抱歉打错了,混淆了 "first" 和 "second")
我对这个特定代码进行了基准测试,即使没有优化,两个变体的运行时间也几乎相同。一旦打开最低级别的优化,结果就非常接近相同(+/- 时间测量中的一点噪音)。
编辑:下面对生成的汇编代码的分析表明,很难猜测哪种形式更快,因为大多数人可能给出的答案是 func2
,但事实证明这个函数有点小速度较慢,至少在使用 clang++ 和 -O2 进行编译时是这样。这很好地证明 "writ code, benchmark, change code, benchmark" 是处理性能的正确方法,而不是根据阅读代码进行猜测。记住有人告诉我的话,优化有点像把洋葱分成几层——一旦你优化了一个部分,你最终会看到非常相似的东西,只是小了一点……;)
然而,我的初步分析使 func1
显着变慢 - 结果证明是因为编译器出于某种奇怪的原因没有优化 func1
中的 rand() % 10000 + 1
但是在关闭优化时在 func2
中执行。这意味着 func1
。但是,一旦启用优化,两个函数都会获得 "fast" 模。
使用 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 59218605, %rcx, %rcx # imm = 0x68DB8BAD
movq %rcx, %rdx
shrq , %rdx
sarq , %rcx
addl %edx, %ecx
imull 000, %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
函数 2:
.LBB1_1:
callq rand
movslq %eax, %rcx
imulq 59218605, %rcx, %rcx # imm = 0x68DB8BAD
movq %rcx, %rdx
shrq , %rdx
sarq , %rcx
addl %edx, %ecx
imull 000, %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 00000, %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)
的结果。这可以解释为什么 func2
比 func1
稍慢。
我确信可以找到 constructing/destroying 一个对象在一个循环中花费大量时间的情况,但一般来说,它几乎没有或根本没有区别,并且代码更清晰实际上会帮助reader。它也经常帮助编译器使用 "life-time analysis",因为它比 "walk" 更少的代码来查明变量是否在以后使用(在这种情况下,代码很短,但显然并不总是现实生活中的案例)