在用C编码时,我遇到了以下情况。
int function ()
{
if (!somecondition) return false;
internalStructure *str1;
internalStructure *str2;
char *dataPointer;
float xyz;
/* do something here with the above local variables */
}
考虑到上面代码中的if
语句可以从函数返回,我可以在两个地方声明变量。
if
陈述之前。if
声明之后。 作为程序员,我会考虑在if
语句之后保留变量声明。
宣言所用的费用是多少?还是有其他理由偏爱另一种方式?
答案 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年更加智能,因此这种限制可以放松,方便开发人员使用。
现代编码实践建议将变量置于其使用范围内
这与编译器无关(显然无论如何都不关心)。已经发现,如果将变量放在接近使用它们的位置,大多数人类程序员都会更好地读取代码。这只是一个风格指南,所以请随意不同意,但开发人员之间有一个非常明显的共识,即这是正确的方式。"
现在针对一些极端情况:
alloca
在此上方的图层上处理。对于那些好奇的人来说,alloca
实现往往具有将堆栈指针向下移动一些任意数量的效果。使用alloca
的函数需要以这种或那种方式跟踪这个空间,并确保在离开之前向上重新调整堆栈指针。alloca
。答案 2 :(得分:22)
在C中,我相信所有变量声明都被应用,就好像它们位于函数声明的顶部;如果你在一个区块中声明它们,我认为它只是一个范围界定的东西(我不认为它在C ++中是相同的)。编译器将对变量执行所有优化,有些甚至可能在更高优化的机器代码中有效消失。然后编译器将决定变量需要多少空间,然后在执行期间创建一个称为变量所在堆栈的空间。
当调用函数时,函数使用的所有变量都会被放入堆栈,以及有关被调用函数的信息(即返回地址,参数等)。声明变量的 无关紧要,只是它被声明了 - 无论如何它都将被分配到堆栈中。
声明变量并不昂贵,"本身;如果它不容易被用作变量,编译器可能会将其作为变量删除。
检查出来:
Wikipedia on call stacks,Some 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 */
}
然后为foo
和somecondition
保留的堆栈空间显然可以重用于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。
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
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
,它会跳转到清理并返回代码),则不会调用其他指令来准备"准备"堆栈变量。唯一的区别是存储在edi
和esi
寄存器中的函数参数a和b以比第一个示例([rbp-4]
更高的地址加载到堆栈中和[rbp - 8]
)。这是因为没有额外的空间被分配"对于像第一个例子中的局部变量。所以,正如你所看到的,唯一的"开销"用于添加这些局部变量的是减法项的变化(即,甚至不添加额外的减法运算)。
因此,在您的情况下,简单地声明堆栈变量几乎没有成本。
答案 11 :(得分:1)
如果在if语句之后声明变量并立即从函数返回,则编译器不会在堆栈中承诺内存。