变量声明是否昂贵?

Who*_*ami 74 c

在用C编码时,我遇到了以下情况.

int function ()
{
  if (!somecondition) return false;

  internalStructure  *str1;
  internalStructure *str2;
  char *dataPointer;
  float xyz;

  /* do something here with the above local variables */    
}
Run Code Online (Sandbox Code Playgroud)

考虑到if上面代码中的语句可以从函数返回,我可以在两个地方声明变量.

  1. if声明之前.
  2. if声明.

作为一名程序员,我认为在ifStatement 之后保留变量声明.

宣言所花费的是什么?还是有其他理由偏爱另一种方式?

Ded*_*tor 95

在C99及更高版本中(或与C89的通用符合扩展),您可以自由地混合语句和声明.

就像在早期版本中一样(只有编译器更聪明,更积极),编译器决定如何分配寄存器和堆栈,或者执行符合as-if-rule的任何其他优化.
这意味着在性能方面,没有任何期望有任何差异.

无论如何,这不是允许的原因:

这是为了限制范围,从而减少人们在解释和验证代码时必须牢记的背景.

  • 如果你想看到优化器在运行,编写一个带声明的函数,初始化像`char*foo ="something"`,用优化器编译代码和调试标志(例如``gcc -O3 -g`)并逐步完成调试器中的函数.步进点将反弹并延迟初始化变量,直到需要它为止. (5认同)
  • "+ 1"注意到在这里减少脑力开销的重要性......很多人都忘了它. (5认同)
  • 简单地说,是的. (2认同)

Cor*_*ica 43

做任何有意义的事情,但是当前的编码风格建议尽可能接近使用变量声明

实际上,在第一个编译器之后,几乎每个编译器都可以使用变量声明.这是因为几乎所有处理器都使用堆栈指针(可能还有帧指针)来管理它们的堆栈.例如,考虑两个功能:

int foo() {
    int x;
    return 5; // aren't we a silly little function now
}

int bar() {
    int x;
    int y;
    return 5; // still wasting our time...
}
Run Code Online (Sandbox Code Playgroud)

如果我要在现代编译器上编译这些(并告诉它不要聪明并优化我未使用的局部变量),我会看到这个(x64汇编示例......其他类似):

foo:
push ebp
mov  ebp, esp
sub  esp, 8    ; 1. this is the first line which is different between the two
mov  eax, 5    ; this is how we return the value
add  esp, 8    ; 2. this is the second line which is different between the two
ret

bar:
push ebp
mov  ebp, esp
sub  esp, 16    ; 1. this is the first line which is different between the two
mov  eax, 5     ; this is how we return the value
add  esp, 16    ; 2. this is the second line which is different between the two
ret
Run Code Online (Sandbox Code Playgroud)

注意:两个函数具有相同数量的操作码!

这是因为几乎所有的编译器都会预先分配他们需要的所有空间(除非alloca是单独处理的花哨的东西).实际上,在x64上,它们必须以这种有效的方式这样做.

(编辑:正如Forss所指出的那样,编译器可能会将一些局部变量优化为寄存器.从技术上讲,我应该争辩说,第一个"溢出"到堆栈中的变量需要花费2个操作码,其余的是免费的)

出于同样的原因,编译器将收集所有局部变量声明,并为它们预先分配空间. C89要求所有声明都是预先设置的,因为它被设计为1遍编译器.为了让C89编译器知道要分配多少空间,它需要在发出其余代码之前知道所有变量.在现代语言中,如C99和C++,编译器应该比1972年更加智能,因此这种限制可以放松,方便开发人员使用.

现代编码实践建议将变量置于其使用范围内

这与编译器无关(显然无论如何都不关心).已经发现,如果将变量放在接近使用它们的位置,大多数人类程序员都会更好地读取代码.这只是一个风格指南,所以请随意不同意它,但开发人员之间有一个非常明显的共识,即这是"正确的方式".

现在针对一些极端情况:

  • 如果您正在使用带有构造函数的C++,编译器将预先分配空间(因为以这种方式执行它会更快,并且不会受到伤害).但是,在代码流中的正确位置之前,不会在该空间中构造变量.在某些情况下,这意味着将变量放在接近其使用的位置甚至可能比将它们放在前面更快 ...流控制可能会引导我们围绕变量声明,在这种情况下甚至不需要调用构造函数.
  • alloca在上面的一层上处理.对于那些好奇的人来说,alloca实现往往具有将堆栈指针向下移动一些任意量的效果.alloca需要使用函数来以这种或那种方式跟踪此空间,并确保在离开之前向上重新调整堆栈指针.
  • 可能存在您通常需要16字节堆栈空间的情况,但在一种情况下,您需要分配50kB的本地数组.无论您将变量放在代码中的哪个位置,每次调用函数时,几乎所有编译器都会分配50kB + 16B的堆栈空间.这很少重要,但在强迫递归的代码中,这可能会溢出堆栈.您必须将使用50kB阵列的代码移动到其自己的函数中,或者使用alloca.
  • 如果分配超过一页的堆栈空间,某些平台(例如:Windows)需要在序言中进行特殊的函数调用.这根本不应该改变分析(在实现中,它是一个非常快速的叶函数,每页只有1个字).


Jer*_*odi 22

在C中,我相信所有变量声明都被应用,就好像它们位于函数声明的顶部; 如果你在一个块中声明它们,我认为它只是一个范围界定(我不认为它在C++中是相同的).编译器将对变量执行所有优化,有些甚至可能在更高优化的机器代码中有效消失.然后编译器将决定变量需要多少空间,然后在执行期间创建一个称为变量所在堆栈的空间.

调用函数时,函数使用的所有变量都会被放入堆栈,以及有关被调用函数的信息(即返回地址,参数等).没关系,其中变量被声明,只是它被宣布-它会被分配到堆栈,不管.

声明变量本身并不"昂贵"; 如果它不容易被用作变量,编译器可能会将其作为变量删除.

看一下这个:

Da堆栈

维基百科上的电话堆栈,堆栈上的其他一些地方

当然,所有这些都依赖于实现和系统.

  • @Paul如果您愿意,将堆栈"反转"并不罕见(如果您手工绘制,堆栈顶部位于纸张顶部具有明显的优势).无论如何,你可能想告诉惠普他们这么多年来一直都做错了(PA-RISC传统上堆栈正在向上发展;)如果你没有`push`和`pop`操作(如果你做的话)实际上任何RISC ISA?)它只是一个惯例 - MULTICS有不断增长的堆栈. (6认同)

Mar*_*mes 11

是的,它可以成本清晰.如果在某种情况下函数必须根本不执行任何操作(例如,在您的情况下找到全局false),那么将检查放在顶部(上面显示的位置)肯定更容易理解 - 调试和/或记录时必不可少的东西.


Bra*_*orm 11

它最终取决于编译器,但通常所有本地都在函数的开头分配.

但是,分配局部变量的成本非常小,因为它们被放在堆栈上(或者在优化后放入寄存器中).

  • 然而,分配只是一个单独的添加指令,所以它是令人难以置信的吱吱声.完全不喜欢使用malloc. (2认同)

bit*_*ask 6

保持声明尽可能接近使用位置.理想情况下,嵌套块内.所以在这种情况下,将变量声明在if语句之上是没有意义的.


Cin*_*out 6

最好的做法是调整一种懒惰的方法,即只在你真正需要它们时声明它们;)(而不是之前).它带来以下好处:

如果这些变量被声明为尽可能靠近使用地点,则代码更具可读性.

  • 这是不正确的(至少对我而言).如果所有变量都在函数顶部的块中声明,而不是分散在代码中,我发现代码更具可读性(并且可编辑 - 我不必去寻找代码来更改声明).正如其他人所指出的那样,编译器足够聪明以优化分配. (4认同)
  • 这是不正确的,期间.该示例使用在堆栈上静态分配的变量.编译器将生成一个指令,以便在堆栈中为这些局部变量保留内存,无论它们在该函数中声明的位置如何. (3认同)
  • 对你来说,也许;但不适用于大多数编码社区! (2认同)

Hag*_*zen 5

如果你有这个

int function ()
{
   {
       sometype foo;
       bool somecondition;
       /* do something with foo and compute somecondition */
       if (!somecondition) return false;
   }
   internalStructure  *str1;
   internalStructure *str2;
   char *dataPointer;
   float xyz;

   /* do something here with the above local variables */    
}
Run Code Online (Sandbox Code Playgroud)

那么栈空间预留foo,并somecondition可以明显重复使用str1等优点,因此通过后声明if,您可以节省堆栈空间.根据编译器的优化能力,堆栈空间节省可能,如果你还通过去除内的一对括号扁平化温控功能发生或者如果你声明str1前等if; 但是,这需要编译器/优化器注意到范围不"真正"重叠.通过在if您之后设置声明即使没有优化也可以促进此行为 - 更不用说改进的代码可读性.


And*_*san 5

无论何时在 C 范围内分配局部变量(例如函数),它们都没有默认的初始化代码(例如 C++ 构造函数)。并且由于它们不是动态分配的(它们只是未初始化的指针),因此不需要调用(例如malloc)额外的(并且可能很昂贵的)函数来准备/分配它们。

由于堆栈的工作方式,分配堆栈变量仅意味着递减堆栈指针(即增加堆栈大小,因为在大多数体系结构中,它向下增长)以便为它腾出空间。从 CPU 的角度来看,这意味着执行一个简单的 SUB 指令:(SUB rsp, 4如果您的变量有 4 个字节大——例如一个常规的 32 位整数)。

此外,当您声明多个变量时,您的编译器足够聪明,可以将它们实际组合成一条大SUB rsp, XX指令,其中XX是作用域局部变量的总大小。理论上。在实践中,会发生一些不同的事情。

在这样的情况下,我发现GCC 资源管理器是一个非常宝贵的工具,可以找出(非常轻松)编译器“幕后”发生的事情。

那么让我们来看看当您实际编写这样的函数时会发生什么:GCC explorer link

代码

int function(int a, int b) {
  int x, y, z, t;

  if(a == 2) { return 15; }

  x = 1;
  y = 2;
  z = 3;
  t = 4;

  return x + y + z + t + a + b;
}
Run Code Online (Sandbox Code Playgroud)

结果组装

function(int, int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-20], edi
    mov DWORD PTR [rbp-24], esi
    cmp DWORD PTR [rbp-20], 2
    jne .L2
    mov eax, 15
    jmp .L3
.L2:
    -- snip --
.L3:
    pop rbp
    ret
Run Code Online (Sandbox Code Playgroud)

事实证明,GCC 比这更聪明。它甚至根本不执行 SUB 指令来分配局部变量。它只是(内部)假设空间被“占用”,但不添加任何指令来更新堆栈指针(例如SUB rsp, XX)。这意味着堆栈指针不会保持最新,但是,因为在这种情况下,在使用堆栈空间后PUSH不再执行指令(并且没有 -rsp相对查找),所以没有问题。

这是一个未声明其他变量的示例:http : //goo.gl/3TV4hE

代码

int function(int a, int b) {
  if(a == 2) { return 15; }
  return a + b;
}
Run Code Online (Sandbox Code Playgroud)

结果组装

function(int, int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov DWORD PTR [rbp-8], esi
    cmp DWORD PTR [rbp-4], 2
    jne .L2
    mov eax, 15
    jmp .L3
.L2:
    mov edx, DWORD PTR [rbp-4]
    mov eax, DWORD PTR [rbp-8]
    add eax, edx
.L3:
    pop rbp
    ret
Run Code Online (Sandbox Code Playgroud)

如果您查看过早返回之前的代码(jmp .L3跳转到清理和返回代码),则不会调用其他指令来“准备”堆栈变量。唯一的区别是存储在ediesi寄存器中的函数参数 a 和 b加载到堆栈中的地址高于第一个示例([rbp-4][rbp - 8])。这是因为没有像第一个示例中那样为局部变量“分配”额外的空间。因此,如您所见,添加这些局部变量的唯一“开销”是减法项的更改(即甚至不添加额外的减法运算)。

因此,在您的情况下,简单地声明堆栈变量几乎没有成本。