变量声明是否昂贵?

时间:2015-01-01 10:24:07

标签: c

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

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

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

  /* do something here with the above local variables */    
}

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

  1. if陈述之前。
  2. if声明之后。
  3. 作为程序员,我会考虑在if语句之后保留变量声明。

    宣言所用的费用是多少?还是有其他理由偏爱另一种方式?

12 个答案:

答案 0 :(得分:94)

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

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

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

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

答案 1 :(得分:42)

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

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

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...
}

如果我要在现代编译器上编译这些(并告诉它不要聪明并优化我未使用的局部变量),我会看到这个(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

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

这是因为几乎所有的编译器都会预先分配他们需要的所有空间(除了alloca这些单独处理的花哨的东西)。实际上,在x64上,强制性是以这种有效的方式实现的。

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

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

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

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

现在针对一些极端情况:

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

答案 2 :(得分:22)

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

当调用函数时,函数使用的所有变量都会被放入堆栈,以及有关被调用函数的信息(即返回地址,参数等)。声明变量的 无关紧要,只是它被声明了 - 无论如何它都将被分配到堆栈中。

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

检查出来:

Da stack

Wikipedia on call stacksSome other place on the stack

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

答案 3 :(得分:11)

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

答案 4 :(得分:11)

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

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

答案 5 :(得分:6)

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

答案 6 :(得分:6)

最佳做法是采用 lazy 方法,即仅在您真正需要时才声明它们;)(而不是之前)。它带来以下好处:

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

答案 7 :(得分: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 */    
}

然后为foosomecondition保留的堆栈空间显然可以重用于str1等,因此通过在if之后声明,可以节省堆栈空间。根据编译器的优化功能,如果您通过移除内部括号来展平功能,或者如果您之前声明str1等,则也可以保存堆栈空间 if;但是,这需要编译器/优化器注意示波器不“真正”重叠。通过在if之后设置声明即使没有优化也可以促进这种行为 - 更不用说改进的代码可读性了。

答案 8 :(得分:4)

除了记录我们为什么要这样做之外,我更喜欢在功能的顶部保持“早出”状态。如果我们把它放在一堆变量声明之后,那些不熟悉代码的人很容易错过它,除非他们知道他们必须寻找它。

单独记录“早出”条件并不总是足够的,最好在代码中明确说明。将早期条件置于顶部也可以更容易地使文档与代码保持同步,例如,如果我们稍后决定删除早期条件,或者添加更多此类条件。

答案 9 :(得分:4)

如果它确实很重要,避免分配变量的唯一方法可能是:

int function_unchecked();

int function ()
{
  if (!someGlobalValue) return false;
  return function_unchecked();
}

int function_unchecked() {
  internalStructure  *str1;
  internalStructure *str2;
  char *dataPointer;
  float xyz;

  /* do something here with the above local variables */    
}

但在实践中,我认为你没有发现任何性能优势。如果有任何微不足道的开销。

当然,如果您正在编写C ++并且其中一些局部变量具有非平凡的构造函数,您可能需要在检查之后放置它们。但即便如此,我也不认为这有助于分裂这个功能。

答案 10 :(得分:4)

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

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

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

在这样的情况下,我发现GCC explorer是一个非常宝贵的工具,可以找到(非常容易)发生的事情"引擎盖下#34;编译器。

因此,让我们来看看当你真正编写这样一个函数时会发生什么:GCC explorer link

C代码

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

结果汇编

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

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

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

C代码

int function(int a, int b) {
  if(a == 2) { return 15; }
  return a + b;
}

结果汇编

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

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

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

答案 11 :(得分:1)

如果在if语句之后声明变量并立即从函数返回,则编译器不会在堆栈中承诺内存。