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

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

例子:

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

   cout << someString;
}

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


当前回答

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

问题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

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

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

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

在您的示例中,程序每次都创建并销毁字符串。一些库使用小字符串优化(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.

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

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

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

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

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

我没有发帖回答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,所以我知道它的工作;此外,我已经尝试使用它在括号外定义的变量,我收到了一个错误,所以我知道变量被“销毁”。

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