variables的声明是昂贵的吗?

在C编码时,我遇到了下面的情况。

int function () { if (!somecondition) return false; internalStructure *str1; internalStructure *str2; char *dataPointer; float xyz; /* do something here with the above local variables */ } 

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

  1. if语句之前。
  2. if语句之后。

作为一名程序员,我想在if语句之后保留variables声明。

申报地点是否有成本? 还是有其他的理由比其他的更喜欢一种方式?

在C99及更高版本中(或者与C89一致的扩展),可以自由组合语句和声明。

就像在早期的版本中一样(编译器变得更聪明,更积极),编译器决定如何分配寄存器和堆栈,或者做任何符合as-if规则的其他优化。
这意味着在性能方面,没有任何区别的期望。

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

这是为了限制范围,从而减less人们在解释和validation代码时必须记住的环境

做一些有意义的事情,但是现在的编码风格build议尽可能使用variables声明

实际上,variables声明在第一个之后的几乎每个编译器都是免费的。 这是因为几乎所有的处理器都用堆栈指针(可能还有一个帧指针)来pipe理堆栈。 例如,考虑两个function:

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

如果我要在现代编译器上编译这些代码(并告诉它不要那么聪明,并优化我的未使用的局部variables),我会看到这个(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所指出的那样,编译器可能会将一些局部variables优化到寄存器中,从技术上讲,我应该争辩说,第一个“溢出”到堆栈中的可变函数需要2个操作码,其余的都是免费的)

出于同样的原因,编译器将收集所有的局部variables声明,并为他们分配空间。 C89要求所有的声明都是预先的,因为它被devise成1路编译器。 为了让C89编译器知道要分配多less空间,在发出其余的代码之前,需要知道所有的variables。 在像C99和C ++这样的现代语言中,编译器要比1972年的要聪明得多,所以这个限制对开发人员来说是很方便的。

现代编码实践build议把variables放在他们的用法上

这与编译器无关(显然不能以这种或那种方式)。 已经发现,如果variables接近于使用的地方,大多数人类程序员会更好地阅读代码。 这只是一个风格指南,所以可以随意不同意,但是开发人员之间有一个非凡的共识,那就是“正确的方式”。

现在有几个例子:

  • 如果你在C ++中使用构造函数,编译器会预先分配空间(因为这样做速度更快,而且不会受到影响)。 但是,只有在代码stream中的正确位置之前,variables才会被构build在该空间中。 在某些情况下,这意味着把variables放在接近它们的地方,甚至可能比把它们放在前面更快 ……stream量控制可能会引导我们绕过variables声明,在这种情况下,甚至不需要调用构造函数。
  • 在这个上面的层上处理alloca 。 对于那些有好奇心的人来说, alloca实现往往alloca栈指针向下移动一定的数量。 使用alloca函数需要以这样或那样的方式跟踪这个空间,并确保堆栈指针在离开之前被重新向上调整。
  • 可能有这种情况,通常需要16个字节的堆栈空间,但是在一种情况下,您需要分配一个50kB的本地数组。 不pipe你把代码放在哪里,几乎所有的编译器在每次调用函数的时候都会分配50kB + 16B的堆栈空间。 这很less有问题,但在沉迷于recursion的代码中,这可能会使堆栈溢出。 您必须将使用50kB数组的代码移入其自己的函数,或者使用alloca
  • 有些平台(如:Windows)需要在序言中使用一个特殊的函数调用,如果你分配了多于一页的栈空间。 这不应该改变非常多的分析(在实施中,这是一个非常快的叶function,只是每页1个字)。

在C中,我相信所有的variables声明都应用在函数声明的顶部。 如果你在一个块中声明它们,我认为这只是一个范围的事情(我不认为它在C ++中是一样的)。 编译器将对variables执行所有的优化,有些甚至可能在更高优化的机器代码中消失。 然后,编译器将决定variables需要多less空间,然后在执行过程中创build一个称为variables所在栈的空间。

当一个函数被调用时,函数使用的所有variables都被放在堆栈上,以及被调用函数的信息(即返回地址,参数等)。 不pipevariables被声明在哪里 ,只要它被声明了 – 它将被分配到栈上。

声明variables本身并不“昂贵” 如果它很容易不被用作variables,编译器可能会将其作为variables移除。

看一下这个:

Da堆栈

维基百科上的调用堆栈 , 在堆栈上的其他地方

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

是的,它可能成本清晰。 如果在某些情况下(如发现全局错误,在你的情况下)函数一定不能做任何事情,那么将这个检查放在顶部,在上面显示的地方肯定会更容易理解 – debugging和/或logging时必不可less的东西。

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

但是,分配局部variables的代价非常小,因为它们被放在堆栈中(或者在优化之后被放入寄存器中)。

保持宣言尽可能接近使用。 理想情况下嵌套块。 所以在这种情况下,在if语句上面声明variables是没有意义的。

如果你有这个

 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之后声明,可以节省堆栈空间。 根据编译器的优化能力,如果你通过移除内部的一对大括号来平坦化函数,或者如果在if之前声明了str1等,也可能会发生堆栈空间的节省。 然而,这需要编译器/优化器注意范围不“真的”重叠。 通过在后面定位声明,即使不进行优化也能促进这种行为 – 更不用说改进的代码可读性了。

最好的做法是适应一个懒惰的方法,也就是说,只有当你真的需要他们时才宣布他们;)(而不是之前)。 这会带来以下好处:

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

除了logging我们为什么要这样做之外,我更喜欢将“早期”状态保留在函数的顶部。 如果我们把它放在一堆variables声明之后,那么不熟悉代码的人就很容易就会错过它,除非他们知道必须去查找它。

单独logging“早期出现”情况并不总是足够的,最好在代码中说清楚。 如果我们稍后决定删除早期状态,或者添加更多这样的条件,那么将早期状态放在最前面也会使保持文档与代码同步变得更容易。

如果它真的重要,避免分配variables的唯一方法可能是:

 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 ++,其中一些局部variables有非平凡的构造函数,你可能需要把它们放在检查之后。 但即使如此,我不认为这将有助于分裂function。

无论何时在C作用域(如函数)中分配局部variables,都没有默认的初始化代码(如C ++构造函数)。 而且由于它们不是dynamic分配的(它们只是未初始化的指针),因此不需要调用额外的(也可能是昂贵的)函数(例如malloc )来准备/分配它们。

由于堆栈的工作方式,分配堆栈variables只是意味着减less堆栈指针(即增加堆栈大小,因为在大多数架构中,堆栈大小会向下增长),以便腾出空间。 从CPU的angular度来看,这意味着执行一条简单的SUB指令: SUB rsp, 4 (如果你的variables是4个字节大的话 – 比如一个32位的整数)。

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

在这样的情况下,我发现GCC explorer是一个非常有价值的工具,用来查找(非常容易)编译器的“底层”情况。

那么让我们来看看当你写这样一个函数时会发生什么: GCC explorer链接 。

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 

事实certificate,GCC甚至比这更聪明。 它甚至不执行SUB指令来分配局部variables。 它只是(内部)假定空间被“占用”,但不会添加任何指令来更新堆栈指针(例如SUB rsp, XX )。 这意味着堆栈指针并不保持最新状态,但是由于在这种情况下在使用堆栈空间后没有执行更多的PUSH指令(也没有rsp相对查找),所以没有问题。

这是一个没有声明额外variables的例子: 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 ,它跳转到清除并返回代码)之前查看代码,则不会调用其他指令来“准备”堆栈variables。 唯一的区别是存储在ediesi寄存器中的函数参数a和b在比第一个例子( [rbp-4][rbp - 8] )更高的地址被加载到堆栈上。 这是因为没有像第一个例子那样为局部variables“分配”额外的空间。 所以,正如你所看到的,添加这些局部variables的唯一“开销”是减法项的变化(即,甚至不增加额外的减法操作)。

所以,就你而言,简单地声明堆栈variables几乎没有成本。

如果在if语句之后声明variables并立即从函数返回,则编译器不在栈中提供内存。