问题1:在循环中声明变量是好做法还是坏做法?

我已经阅读了其他关于是否存在性能问题的线程(大多数人说没有),并且您应该始终在接近它们将被使用的地方声明变量。我想知道的是,这种情况是否应该避免,还是更可取。

例子:

for(int counter = 0; counter <= 10; counter++)
{
   string someString = "testing";

   cout << someString;
}

问题2:大多数编译器是否意识到该变量已经被声明,而只是跳过这一部分,还是每次都在内存中为它创建一个位置?


当前回答

这是一个很好的实践,因为所有以上的答案都提供了非常好的理论方面的问题,让我看了一下代码,我试图解决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。试一试....你会明白的。希望这对你有所帮助。

其他回答

第4.8章K&R的C程序设计语言中的块结构

类中声明并初始化的自动变量 每次输入块时,块都被初始化。

我可能没有在书中看到相关的描述,比如:

类中声明并初始化的自动变量 在输入块之前,只分配一次块。

但一个简单的测试可以证明这个假设:

 #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;                                                                                                         
 }                                                                                                                     

既然你的第二个问题比较具体,我就先回答你的第二个问题,然后结合第二个问题的背景回答你的第一个问题。我想给出一个比现有的更有根据的答案。

问题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.01秒。毕竟,这只是循环中的一个额外操作。(对于字符串,循环内操作的差异显然要大一些,但不是很明显。)

而且,这个问题很大程度上是学术问题,因为在优化级别为-O1或更高时,gcc为两个源文件输出相同的asm, clang也是如此。因此,至少对于这种简单的情况,它不太可能对性能产生任何影响。当然,在实际的程序中,您应该始终进行分析,而不是进行假设。

问题#1:在循环中声明变量是一种好做法还是 糟糕的实践?

As with practically every question like this, it depends. If the declaration is inside a very tight loop and you're compiling without optimizations, say for debugging purposes, it's theoretically possible that moving it outside the loop would improve performance enough to be handy during your debugging efforts. If so, it might be sensible, at least while you're debugging. And although I don't think it's likely to make any difference in an optimized build, if you do observe one, you/your pair/your team can make a judgement call as to whether it's worth it.

At the same time, you have to consider not only how the compiler reads your code, but also how it comes off to humans, yourself included. I think you'll agree that a variable declared in the smallest scope possible is easier to keep track of. If it's outside the loop, it implies that it's needed outside the loop, which is confusing if that's not actually the case. In a big codebase, little confusions like this add up over time and become fatiguing after hours of work, and can lead to silly bugs. That can be much more costly than what you reap from a slight performance improvement, depending on the use case.

下面的两个代码段生成相同的程序集。

// snippet 1
void test() { 
   int var; 
   while(1) var = 4;
}


// snippet 2
void test() {
    while(1) int var = 4;
}

输出:

test():
        push    rbp
        mov     rbp, rsp
.L2:
        mov     DWORD PTR [rbp-4], 4
        jmp     .L2

链接:https://godbolt.org/z/36hsM6Pen

因此,除非涉及到分析或计算扩展构造函数,否则保持声明接近其用法应该是默认方法。

这是很好的练习。

通过在循环内部创建变量,可以确保它们的作用域限制在循环内部。它不能在循环之外被引用或调用。

这种方式:

If the name of the variable is a bit "generic" (like "i"), there is no risk to mix it with another variable of same name somewhere later in your code (can also be mitigated using the -Wshadow warning instruction on GCC) The compiler knows that the variable scope is limited to inside the loop, and therefore will issue a proper error message if the variable is by mistake referenced elsewhere. Last but not least, some dedicated optimization can be performed more efficiently by the compiler (most importantly register allocation), since it knows that the variable cannot be used outside of the loop. For example, no need to store the result for later re-use.

总之,你这样做是对的。

但是请注意,变量不应该在每个循环之间保留其值。在这种情况下,您可能每次都需要初始化它。您还可以创建一个更大的块,包括循环,其唯一目的是声明变量,这些变量必须在一个循环到另一个循环中保持其值。这通常包括循环计数器本身。

{
    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 { (...) }
    }

差异可能看起来很小,尤其是在这样一个小例子中。 但在更大的代码基础上,这将有所帮助:现在从f1()块传输一些结果值到f2()块没有风险。每个结果都严格限制在自己的范围内,使其作用更加准确。从审阅者的角度来看,这样更好,因为他不必担心和跟踪长范围的状态变量。

甚至编译器也会提供更好的帮助:假设在将来,在一些错误的代码更改之后,result没有正确地用f2()初始化。第二个版本将简单地拒绝工作,在编译时声明一个明确的错误消息(比运行时好得多)。第一个版本不会发现任何东西,f1()的结果将被简单地测试第二次,与f2()的结果混淆。

补充信息

开源工具CppCheck (C/ c++代码的静态分析工具)提供了一些关于变量最佳作用域的极好提示。

在回应有关编配的评论时: 上面的规则在C中是正确的,但对于某些c++类可能不是。

对于标准类型和结构,变量的大小在编译时已知。在C语言中没有“构造”这样的东西,所以当函数被调用时,变量的空间将被简单地分配到堆栈中(没有任何初始化)。这就是为什么在循环中声明变量时代价为“零”的原因。

然而,对于c++类,有一个构造函数的东西,我知道得少得多。我想分配可能不是问题,因为编译器应该足够聪明,可以重用相同的空间,但初始化很可能发生在每次循环迭代中。

从前(c++ 98之前);以下将中断:

{
    for (int i=0; i<.; ++i) {std::string foo;}
    for (int i=0; i<.; ++i) {std::string foo;}
}

警告我已经声明(foo是好的,因为它的范围在{})。这可能是人们首先认为它不好的原因。但很久以前就不是这样了。

如果你仍然要支持这样一个旧的编译器(有些人是Borland),那么答案是肯定的,一个情况下可以把i的循环,因为不这样做使得它使它“更难”的人把多个循环与相同的变量,虽然老实说编译器仍然会失败,这是所有你想要的,如果有一个问题。

如果你不再需要支持这样一个旧的编译器,变量应该保持在你能得到的最小范围内,这样你不仅可以最小化内存使用;但也使项目更容易理解。这有点像问为什么不让所有变量都是全局变量。同样的论点也适用,但作用域略有变化。