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

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

例子:

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

   cout << someString;
}

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


当前回答

一般来说,把它放在很近的地方是一个很好的做法。

在某些情况下,出于性能等考虑,需要将变量从循环中取出。

在您的示例中,程序每次都创建并销毁字符串。一些库使用小字符串优化(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的情况下,“空格”包含一个指针,其中包含表示字符的动态分配。所以这个:

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:大多数编译器是否意识到变量已经 被声明过,跳过这部分,或者它实际上创建了一个 每次都在记忆中找到它?

您可以通过在运行汇编程序之前停止编译器并查看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.

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

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

// 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

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

对于c++,这取决于你在做什么。 好吧,这是愚蠢的代码,但想象一下

类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秒才能完成。

你将需要5秒钟,直到你得到myOtherFunc的输出。

当然,这是一个疯狂的例子。

但它说明,当构造函数和/或析构函数需要一些时间时,执行相同构造的每个循环可能会产生性能问题。

我没有发帖回答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”、“while”等后面。

我的代码编译在微软Visual c++ 2010 Express,所以我知道它的工作;此外,我已经尝试使用它在括号外定义的变量,我收到了一个错误,所以我知道变量被“销毁”。

我不知道使用这种方法是否是不好的做法,因为许多未标记的括号会很快使代码无法阅读,但也许一些注释可以澄清这些问题。