是否在每次迭代中重新声明变量比在每次迭代后重置它们更快?

时间:2016-06-08 16:59:06

标签: c performance

所以我对两种不同代码技术的性能有疑问。你能帮我理解哪一个更快/更好,为什么?

这是第一种技术:

int x, y, i;
for(i=0; i<10; i++)
{
    //do stuff with x and y
}
//reset x and y to zero
x=0; 
y=0;

这是第二个:

int i;
for(i=0; i<10; i++)
{
    int x, y;
    //do the same stuff with x and y as above
}

那么哪种编码技术更好? 此外,如果你知道一个更好的和/或任何网站/文章等我可以阅读有关这个​​和更多性能相关的东西,我也希望有这个!

5 个答案:

答案 0 :(得分:1)

在最内部范围内声明变量,您将使用它们:

int i;
for(i=0; i<10; i++)
{
    int x, y;
    //do the same stuff with x and y as above
}

始终是首选。最大的改进是您限制了xy变量的范围。这可以防止您在没有意图的情况下意外使用它们。

即使你使用&#34;同样的&#34;变量:

int i;
for(i=0; i<10; i++)
{
    int x, y;
    //do the same stuff with x and y as above
}

for(i=0; i<10; i++)
{
    int x, y;
    //do the same stuff with x and y as above
}

不会对性能产生任何影响。语句int x, y在运行时几乎没有效果。

大多数现代编译器将计算所有局部变量的总大小,并发出代码以在函数序言中保留堆栈上的空间(例如sub esp, 90h)。这些变量的空间几乎肯定会从一个版本中重复使用。 x到下一个。它纯粹是一个词法结构,编译器使用它来阻止你使用#34; space&#34;在你没有打算的堆栈上。

答案 1 :(得分:0)

它应该无关紧要,因为在任何一种情况下都需要初始化变量。此外,第一种情况设置x和y 后不再使用它们。因此,不需要重置。

这是第一种技术:

int x=0, y=0, i;
for(i=0; i<10; i++)
{
    //do stuff with x and y
    // x and y stay at the value they get set to during the pass
}
// x and y need to be reset if you want to use them again.
// or would retain whatever they became during the last pass.

如果您希望在循环内将x和y重置为0,那么您需要说

这是第一种技术:

int x, y, i;
for(i=0; i<10; i++)
{
    //reset x and y to zero
    x=0; 
    y=0;
    //do stuff with x and y
    // Now x and y get reset before the next pass
}

第二个过程使x和y在范围内是局部的,因此它们在最后一次传递结束时被删除。这些值保留了每次传递期间为下一次传递设置的任何值。编译器实际上会设置变量并在编译时将其初始化为而不是在运行时。因此,您不会为循环中的每次传递定义(和初始化)变量。

这是第二个:

int i;
for(i=0; i<10; i++)
{
    int x=0, y=0;
    //do the same stuff with x and y as above
    // Usually x and y only saet to 0 at start of first pass.
}

答案 2 :(得分:0)

最佳实践

  

那么哪种编码技术更好?

正如其他人所指出的那样,给定一个足够成熟/现代的编译器,由于优化,性能方面可能会为空。相反,首选代码是通过称为最佳实践的一组想法来确定的。

限制范围

&#34;适用范围&#34;描述了代码中的range of access。假设预期范围仅限于循环内部,xy应在循环中声明为,因为编译器将阻止您稍后使用它们你的功能。但是,在您的OP中,您显示它们正在重置,这意味着它们将在以后再次用于其他目的。在这种情况下,必须将它们声明为顶部(例如循环外),以便稍后使用它们。

这里有一些代码可用于证明范围的限制:

#include <stdio.h>

#define IS_SCOPE_LIMITED

int main ( void )
{
  int i;

#ifndef IS_SCOPE_LIMITED
  int x, y;                 // compiler will not complain, scope is generous
#endif

  for(i=0; i<10; i++)
  {
#ifdef IS_SCOPE_LIMITED
    int x, y;              // compiler will complain about use outside of loop
#endif
    x = i;
    y = x+1;
    y++;
  }

  printf("X is %d and Y is %d\n", x, y);
}

要测试范围,请将#define注释到顶部。使用gcc -Wall loopVars.c -o loopVars进行编译,然后使用./loopVars运行。

基准测试和分析

如果您仍然关注性能,可能是因为您有一些涉及这些变量的模糊操作,那么再次测试,测试和测试!(尝试benchmarking或{ {3}}你的代码)。同样,通过优化,您可能无法发现重要(如果有)差异,因为编译器将在运行时之前完成所有这些(变量空间的分配)。

更新

要以另一种方式演示,您可以从代码中删除#ifdef#ifndef(同时删除每个#endif),然后在printf之前添加一行比如x=2; y=3;。你会发现代码将编译并运行,但输出将是&#34; X是2,Y是3&#34;。这是合法的,因为这两个范围阻止了同名变量相互竞争。当然,这是一个坏主意,因为你现在在同一段代码中有多个变量,这些变量具有相同的名称和更复杂的代码,这将不易于阅读和维护。

答案 3 :(得分:0)

根本不重要,因为编译器不会自动将变量声明转换为内存或寄存器分配。两个样本之间的差异在于,在第一种情况下,变量在循环体外部是可见的,而在第二种情况下它们不是。但是,这种差异仅在C级别,如果不在循环外使用变量,则会产生相同的编译代码。

编译器有两个选项可以存储局部变量:它位于堆栈上或寄存器中。对于您在程序中使用的每个变量,编译器必须选择它将存在的位置。如果在堆栈上,则需要递减堆栈指针以为变量腾出空间。但是这种递减不会发生在变量声明的地方,通常它会在函数的开头完成:堆栈指针只会递减一次足以容纳所有堆栈分配变量的量。如果它只是在寄存器中,则不需要进行初始化,并且当您第一次执行赋值时,寄存器将用作目标。重要的是,它可以并将重复使用以前用于现在超出范围的变量的存储器位置和寄存器。

为了说明,我制作了两个测试程序。我使用了10000次迭代而不是10次,因为否则编译器会在高优化级别展开循环。程序使用rand进行快速便携的演示,但不应在生产代码中使用。

declare_once.c:

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main(void) {
    srand(time(NULL));

    int x, y, i;
    for (i = 0; i < 10000; i++) {
        x = rand();
        y = rand();
        printf("Got %d and %d !\n", x, y);
    }

    return 0;
}
除了循环之外,

redeclare.c是相同的:

for (i = 0; i < 10000; i++) {
    int x, y;
    x = rand();
    y = rand();
    printf("Got %d and %d !\n", x, y);
}

我在x86_64 Mac上使用Apple的LLVM版本7.3.0编译了程序。我问了下面转载的装配输出,省略了与问题无关的部分。

clang -O0 -S declare_once.c -o declare_once.S:

_main:
## Function prologue
    pushq   %rbp
    movq    %rsp, %rbp           ## Move the old value of the stack 
                                 ## pointer (%rsp) to the base pointer 
                                 ## (%rbp), which will be used to 
                                 ## address stack variables

    subq    $32, %rsp            ## Decrement the stack pointer by 32 
                                 ## to make room for up to 32 bytes 
                                 ## worth of stack variables including 
                                 ## x and y

## Removed code that calls srand

    movl    $0, -16(%rbp)        ## i = 0. i has been assigned to the 4 
                                 ## bytes starting at address -16(%rbp),
                                 ## which means 16 less than the base  
                                 ## pointer (so here, 16 more than the 
                                 ## stack pointer).

LBB0_1:                                 
    cmpl    $10, -16(%rbp)
    jge LBB0_4
    callq   _rand                ## Call rand. The return value will be in %eax

    movl    %eax, -8(%rbp)       ## Assign the return value of rand to x. 
                                 ## x has been assigned to the 4 bytes
                                 ## starting at -8(%rbp)
    callq   _rand
    leaq    L_.str(%rip), %rdi
    movl    %eax, -12(%rbp)      ## Assign the return value of rand to y. 
                                 ## y has been assigned to the 4 bytes
                                 ## starting at -12(%rbp)
    movl    -8(%rbp), %esi
    movl    -12(%rbp), %edx
    movb    $0, %al
    callq   _printf
    movl    %eax, -20(%rbp)
    movl    -16(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -16(%rbp)
    jmp LBB0_1
LBB0_4:
    xorl    %eax, %eax
    addq    $32, %rsp            ## Add 32 to the stack pointer : 
                                 ## deallocate all stack variables 
                                 ## including x and y
    popq    %rbp
    retq

redeclare.c的程序集输出几乎完全相同,只是由于某种原因x和y分别分配给-16(%rbp)-12(%rbp)i被分配给{ {1}}。我只复制粘贴循环:

-8(%rbp)

所以我们看到即使在-O0,生成的代码也是一样的。需要注意的重要一点是,在每次循环迭代中,相同的内存位置会重复用于x和y,即使它们在C语言的每个迭代中都是单独的变量。

在-O3处,变量保存在寄存器中,两个程序都输出完全相同的程序集

clang -O3 -S declare_once.c -o declare_once.S:

    movl    $0, -16(%rbp)
LBB0_1:
    cmpl    $10, -16(%rbp)
    jge LBB0_4
    callq   _rand
    movl    %eax, -8(%rbp)        ## x = rand();
    callq   _rand
    leaq    L_.str(%rip), %rdi
    movl    %eax, -12(%rbp)       ## y = rand();
    movl    -8(%rbp), %esi
    movl    -12(%rbp), %edx
    movb    $0, %al
    callq   _printf
    movl    %eax, -20(%rbp)
    movl    -16(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -16(%rbp)
    jmp LBB0_1

同样,两个版本之间没有差异,即使在redeclare.c中我们在每次迭代时都有不同的变量,也会重复使用相同的寄存器,这样就不会有分配开销。

请记住,我所说的所有内容都适用于在每次循环迭代中分配的变量,这似乎就是您的想法。另一方面,如果要对所有迭代使用相同的值,当然应该在循环之前完成赋值。

答案 4 :(得分:0)

int变量的特定情况下,它几乎没有(或没有)差异。

对于更复杂类型的变量,特别是具有(例如)动态分配一些内存的构造函数的东西,在循环的每次迭代中重新创建变量可能比重新初始化它要慢得多。例如:

#include <vector>
#include <chrono>
#include <numeric>
#include <iostream>

unsigned long long versionA() {
    std::vector<int> x;
    unsigned long long total = 0;

    for (int j = 0; j < 1000; j++) {
        x.clear();
        for (int i = 0; i < 1000; i++)
            x.push_back(i);
        total += std::accumulate(x.begin(), x.end(), 0ULL);
    }
    return total;
}

unsigned long long versionB() {
    unsigned long long total = 0;

    for (int j = 0; j < 1000; j++) {
        std::vector<int> x;
        for (int i = 0; i < 1000; i++)
            x.push_back(i);
        total += std::accumulate(x.begin(), x.end(), 0ULL);
    }
    return total;
}

template <class F>
void timer(F f) {
    using namespace std::chrono;

    auto start = high_resolution_clock::now();
    auto result = f();
    auto stop = high_resolution_clock::now();

    std::cout << "Result: " << result << "\n";
    std::cout << "Time:   " << duration_cast<microseconds>(stop - start).count() << "\n";
}

int main() {
    timer(versionA);
    timer(versionB);
}

至少在我运行它时,两种方法之间存在相当大的差异:

Result: 499500000
Time:   5114
Result: 499500000
Time:   13196

在这种情况下,每次迭代创建一个新向量所需的时间是每次迭代清除现有向量的两倍多。

对于它的价值,可能有两个因素导致速度差异:

  1. 初始创建矢量。
  2. 重新分配内存,因为元素会添加到矢量中。
  3. 当我们clear()向量时,删除现有元素,但保留当前分配的内存,所以在这种情况下,我们在外循环的每次迭代中都使用相同的大小,即只是重置向量不需要在后续迭代中分配任何内存。如果我们在x.reserve(1000);中定义向量后立即添加vesionA,则差异会大幅缩小(至少在我的测试中速度不是很紧,但非常接近)。