问题#1:在循环中声明变量是一种好习惯还是坏习惯?
我已经阅读了其他关于是否存在性能问题的线程(大多数说没有),并且您应该始终将变量声明为接近它们将被使用的位置。我想知道的是,这是否应该避免,或者是否真的是首选。
示例:
for(int counter = 0; counter <= 10; counter++)
{
string someString = "testing";
cout << someString;
}
问题#2:大多数编译器是否已经声明变量已经声明并且只跳过该部分,或者每次都在内存中为它创建一个点?
答案 0 :(得分:269)
这是优秀的练习。
通过在循环内创建变量,可以确保它们的范围仅限于循环内部。它不能被引用,也不能在循环之外调用。
这样:
如果变量的名称有点“通用”(如“i”),则没有风险将其与代码中稍后某处的同名变量混合使用(也可以使用关于海湾合作委员会的-Wshadow
警告指示
编译器知道变量范围仅限于循环内部,因此如果变量在其他地方被错误引用,则会发出正确的错误消息。
最后但并非最不重要的是,编译器可以更有效地执行某些专用优化(最重要的是寄存器分配),因为它知道变量不能在循环之外使用。例如,无需存储结果以供以后重复使用。
简而言之,你是对的。
但请注意,变量不应在每个循环之间保留其值。在这种情况下,您可能需要每次都初始化它。您还可以创建一个更大的块,包含循环,其唯一目的是声明必须将其值从一个循环保留到另一个循环的变量。这通常包括循环计数器本身。
{
int i, retainValue;
for (i=0; i<N; i++)
{
int tmpValue;
/* tmpValue is uninitialized */
/* retainValue still has its previous value from previous loop */
/* Do some stuff here */
}
/* Here, retainValue is still valid; tmpValue no longer */
}
对于问题#2: 调用函数时,变量被分配一次。实际上,从分配的角度来看,它几乎与在函数开头声明变量相同。唯一的区别是范围:变量不能在循环外使用。甚至可能没有分配变量,只是重新使用一些空闲槽(来自其范围已经结束的其他变量)。
通过限制且更精确的范围,可以获得更准确的优化。但更重要的是,它使您的代码更安全,在阅读代码的其他部分时需要担心更少的状态(即变量)。
即使在if(){...}
块之外也是如此。通常,而不是:
int result;
(...)
result = f1();
if (result) then { (...) }
(...)
result = f2();
if (result) then { (...) }
写起来更安全:
(...)
{
int const result = f1();
if (result) then { (...) }
}
(...)
{
int const result = f2();
if (result) then { (...) }
}
差异似乎很小,特别是在这么小的例子上。
但是在更大的代码库中,它会有所帮助:现在没有风险将result
值从f1()
传输到f2()
块。每个result
都严格限制在自己的范围内,使其角色更加准确。从审稿人的角度来看,它更好,因为他有更少的远程状态变量来担心和跟踪。
即使编译器也会提供更好的帮助:假设在将来错误更改代码之后,result
未使用f2()
正确初始化。第二个版本将简单地拒绝工作,在编译时声明一个明确的错误消息(比运行时更好)。第一个版本不会发现任何内容,f1()
的结果只会被第二次测试,对f2()
的结果感到困惑。
开源工具CppCheck(C / C ++代码的静态分析工具)提供了一些关于变量最佳范围的优秀提示。
回应对分配的评论: 上述规则在C中是正确的,但可能不适用于某些C ++类。
对于标准类型和结构,变量的大小在编译时是已知的。在C中没有“构造”这样的东西,所以当调用函数时,变量的空间将被简单地分配到堆栈中(没有任何初始化)。这就是在循环中声明变量时出现“零”成本的原因。
但是,对于C ++类,有一些我不太了解的构造函数。我认为分配可能不会成为问题,因为编译器应该足够聪明以重用相同的空间,但初始化很可能在每次循环迭代时发生。
答案 1 :(得分:17)
一般来说,保持非常接近是非常好的做法。
在某些情况下,会有一些考虑因素,例如将变量拉出循环的理由。
在您的示例中,程序每次都会创建并销毁字符串。有些库使用小型字符串优化(SSO),因此在某些情况下可以避免动态分配。
假设您想要避免那些多余的创建/分配,您可以将其写为:
for (int counter = 0; counter <= 10; counter++) {
// compiler can pull this out
const char testing[] = "testing";
cout << testing;
}
或者你可以拉出常数:
const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
cout << testing;
}
大多数编译器是否已经声明变量已经被声明并且只是跳过那个部分,或者每次都在内存中为它创建一个点?
它可以重用变量消耗的空间,并且可以将不变量拉出循环。在const char数组的情况下(上图) - 该数组可以被拉出。但是,对于对象(例如std::string
),必须在每次迭代时执行构造函数和析构函数。在std::string
的情况下,'space'包括一个指针,该指针包含表示字符的动态分配。所以这个:
for (int counter = 0; counter <= 10; counter++) {
string testing = "testing";
cout << testing;
}
在每种情况下都需要冗余复制,如果变量高于SSO字符数阈值(并且SSO由std库实现),则需要动态分配和空闲。
这样做:
string testing;
for (int counter = 0; counter <= 10; counter++) {
testing = "testing";
cout << testing;
}
在每次迭代时仍然需要字符的物理副本,但是表单可能会导致一次动态分配,因为您分配了字符串,并且实现应该看到不需要调整字符串的后备分配。当然,在这个例子中你不会这样做(因为已经演示了多个优秀的替代品),但是当字符串或矢量的内容变化时你可能会考虑它。
那么您如何处理所有这些选项(以及更多)?默认情况下保持非常接近 - 直到您了解成本并知道何时应该偏离。
答案 2 :(得分:12)
对于C ++,它取决于你在做什么。 好吧,这是愚蠢的代码,但想象
class myTimeEatingClass { public: //constructor myTimeEatingClass() { sleep(2000); ms_usedTime+=2; } ~myTimeEatingClass() { sleep(3000); ms_usedTime+=3; } const unsigned int getTime() const { return ms_usedTime; } static unsigned int ms_usedTime; };
myTimeEatingClass::ms_CreationTime=0;
myFunc()
{
for (int counter = 0; counter <= 10; counter++) {
myTimeEatingClass timeEater();
//do something
}
cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;
}
myOtherFunc()
{
myTimeEatingClass timeEater();
for (int counter = 0; counter <= 10; counter++) {
//do something
}
cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;
}
您将等待55秒,直到获得myFunc的输出。 仅仅因为每个循环构造函数和析构函数一起需要5秒才能完成。
在获得myOtherFunc的输出之前,您需要5秒钟。
当然,这是一个疯狂的例子。
但它说明当构造函数和/或析构函数需要一些时间时,每个循环完成相同的构造时,它可能会成为一个性能问题。
答案 3 :(得分:8)
我没有发帖回答JeremyRR的问题(因为他们已经回答过了);相反,我发布的仅仅是提出建议。
对于JeremyRR,你可以这样做:
{
string someString = "testing";
for(int counter = 0; counter <= 10; counter++)
{
cout << someString;
}
// The variable is in scope.
}
// The variable is no longer in scope.
我不知道你是否意识到(当我第一次开始编程时没有),括号(只要它们成对)可以放在代码中的任何地方,而不仅仅是在“if”,“for之后“,”而“等等。
我的代码在Microsoft Visual C ++ 2010 Express中编译,所以我知道它有效;另外,我试图在它所定义的括号之外使用变量,我收到一个错误,所以我知道该变量被“销毁”。
我不知道使用这种方法是否是不好的做法,因为许多未标记的括号可能很快使代码无法读取,但也许某些注释可能会清除。
答案 4 :(得分:0)
这是一个很好的实践,因为以上所有答案都提供了很好的理论方面的问题,让我瞥了一眼代码,我试图通过GEEKSFORGEEKS解决DFS,遇到了优化问题…… 如果您尝试解决代码,则在循环外声明整数将为您带来优化错误。
stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
int flag=0;
int top=0;
while(!st.empty()){
top = st.top();
for(int i=0;i<g[top].size();i++){
if(vis[g[top][i]] != 1){
st.push(g[top][i]);
cout<<g[top][i]<<" ";
vis[g[top][i]]=1;
flag=1;
break;
}
}
if(!flag){
st.pop();
}
}
现在将整数放入循环内,这将为您提供正确的答案...
stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
// int flag=0;
// int top=0;
while(!st.empty()){
int top = st.top();
int flag = 0;
for(int i=0;i<g[top].size();i++){
if(vis[g[top][i]] != 1){
st.push(g[top][i]);
cout<<g[top][i]<<" ";
vis[g[top][i]]=1;
flag=1;
break;
}
}
if(!flag){
st.pop();
}
}
这完全反映了@justin先生在第二条评论中说的话...。 在这里试试 https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1。试一试...。您将得到它。希望获得帮助。
答案 5 :(得分:0)
第4.8章块结构(使用K&R的 C编程语言2.Ed。:
)在变量中声明和初始化的自动变量 每次输入块时都会初始化该块。
我可能会错过看书中的相关描述,例如:
在变量中声明和初始化的自动变量 在进入该区块之前,该区块仅分配了一次。
但是简单的测试可以证明假设成立:
#include <stdio.h>
int main(int argc, char *argv[]) {
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
int k;
printf("%p\n", &k);
}
}
return 0;
}
答案 6 :(得分:0)
由于您的第二个问题更具体,因此我将首先解决它,然后根据第二个问题给出的上下文来讨论您的第一个问题。我想给出一个比现在更多的基于证据的答案。
问题2 :大多数编译器是否意识到该变量已经 被声明,然后跳过该部分,或者实际上创建了一个 每次都在内存中找到它吗?
您可以通过在运行汇编器之前停止编译器并查看asm来自己回答此问题。 (如果编译器具有gcc风格的界面,请使用-S
标志;如果要使用我在此处使用的语法样式,请使用-masm=intel
标志。)
在任何情况下,对于x86-64的现代编译器(gcc 10.2,clang 11.0),如果禁用优化,它们只会在每次循环传递时重新加载变量。考虑以下C ++程序-为了直观地映射到asm,我将大部分内容保留为C样式,并使用整数而不是字符串,尽管在字符串情况下适用相同的原理:
#include <iostream>
static constexpr std::size_t LEN = 10;
void fill_arr(int a[LEN])
{
/* *** */
for (std::size_t i = 0; i < LEN; ++i) {
const int t = 8;
a[i] = t;
}
/* *** */
}
int main(void)
{
int a[LEN];
fill_arr(a);
for (std::size_t i = 0; i < LEN; ++i) {
std::cout << a[i] << " ";
}
std::cout << "\n";
return 0;
}
我们可以将其与具有以下区别的版本进行比较:
/* *** */
const int t = 8;
for (std::size_t i = 0; i < LEN; ++i) {
a[i] = t;
}
/* *** */
在禁用优化的情况下,对于循环内声明版本,gcc 10.2在循环的每一遍都将8放入堆栈中:
mov QWORD PTR -8[rbp], 0
.L3:
cmp QWORD PTR -8[rbp], 9
ja .L4
mov DWORD PTR -12[rbp], 8 ;✷
而对于循环外版本它只执行一次:
mov DWORD PTR -12[rbp], 8 ;✷
mov QWORD PTR -8[rbp], 0
.L3:
cmp QWORD PTR -8[rbp], 9
ja .L4
这会对性能产生影响吗?直到我将迭代次数推至数十亿次之后,我才发现它们与我的CPU(Intel i7-7700K)在运行时间上没有明显的差异,即使那时平均差异还不到0.01s。毕竟,这只是循环中的一个额外操作。 (对于一个字符串,循环内操作的差异显然要大一些,但并不是那么明显。)
此外,这个问题在很大程度上是学术上的,因为优化级别为-O1
或更高的gcc对于两个源文件都输出相同的asm,与clang一样。因此,至少对于像这样的简单情况,不可能以任何一种方式对性能产生任何影响。当然,在现实世界中的程序中,您应该始终概述而不是进行假设。
问题#1 :在循环中声明变量是一种好的做法,还是 不好的做法?
与几乎每个这样的问题一样,它取决于。如果声明在一个非常紧密的循环中,并且您没有进行优化就进行编译(例如出于调试目的),则从理论上讲,将其移出循环将有可能提高性能,从而在调试工作中非常方便。如果是这样,至少在调试时,这可能是明智的。而且,尽管我认为优化的构建可能不会有任何改变,但是如果您确实观察到了,则您/您的团队/您的团队可以就是否值得进行判断。
同时,您不仅必须考虑编译器如何读取您的代码,还必须考虑它如何对人类(包括您自己)产生影响。我认为您会同意,在最小范围内声明的变量更易于跟踪。如果它在循环之外,则意味着在循环外需要它,如果实际并非如此,这会造成混淆。在大型代码库中,随着时间的流逝,诸如此类的小混乱逐渐加重,并在工作数小时后变得疲劳,并可能导致愚蠢的错误。根据使用情况,这可能会比通过稍微提高性能而获得的成本高得多。
答案 7 :(得分:0)
在循环内或循环外声明变量,这是JVM规范的结果,但以最佳编码实践的名义,建议在尽可能小的范围内声明变量(在本例中,它在循环内,因为这是唯一使用变量的地方)。在最小范围内声明对象可以提高可读性。局部变量的范围应该总是尽可能的小。在你的例子中,我假设 str 没有在 while 循环之外使用,否则你不会问这个问题,因为在 while 循环中声明它不会是一个选项,因为它不会编译。
如果我在 a 内部或外部声明变量会有所不同,如果我在 Java 中在循环内部或外部声明变量有什么不同吗?这是 for(int i = 0; i < 1000; i++) { int 在单个变量的层面上,效率没有显着差异,但如果你有一个有 1000 个循环和 1000 个变量的函数(别介意坏风格暗示)可能存在系统差异,因为所有变量的所有生命周期都相同而不是重叠。
在 for 循环中声明循环控制变量,当你在 for 循环中声明一个变量时,有一点需要记住:当 for 语句执行时,该变量的作用域结束。 (也就是说,变量的范围仅限于 for 循环。)这个 Java 示例展示了如何使用声明块在 Java For 循环中声明多个变量。
答案 8 :(得分:0)
从前(C++98 之前);以下将中断:
{
for (int i=0; i<.; ++i) {std::string foo;}
for (int i=0; i<.; ++i) {std::string foo;}
}
警告我已经声明(foo 很好,因为它在 {} 范围内)。这很可能是人们首先认为它不好的原因。不过很久以前就不再是真的了。
如果您仍然必须支持这样一个旧的编译器(有些人在 Borland 上),那么答案是肯定的,可以将 i 排除在循环之外,因为不这样做会使它变得“更难”让人们将多个循环放入同一个变量中,但老实说,编译器仍然会失败,如果出现问题,这就是你想要的。
如果你不再需要支持这样一个旧的编译器,变量应该保持在你可以获得的最小范围内,这样你不仅可以最大限度地减少内存使用;但也使理解项目更容易。这有点像问为什么不将所有变量设为全局变量。相同的论点适用,但范围略有变化。