在用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
上面代码中的语句可以从函数返回,我可以在两个地方声明变量.
if
声明之前.if
声明.作为一名程序员,我认为在if
Statement 之后保留变量声明.
宣言所花费的是什么?还是有其他理由偏爱另一种方式?
Ded*_*tor 95
在C99及更高版本中(或与C89的通用符合扩展),您可以自由地混合语句和声明.
就像在早期版本中一样(只有编译器更聪明,更积极),编译器决定如何分配寄存器和堆栈,或者执行符合as-if-rule的任何其他优化.
这意味着在性能方面,没有任何期望有任何差异.
无论如何,这不是允许的原因:
这是为了限制范围,从而减少人们在解释和验证代码时必须牢记的背景.
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年更加智能,因此这种限制可以放松,方便开发人员使用.
现代编码实践建议将变量置于其使用范围内
这与编译器无关(显然无论如何都不关心).已经发现,如果将变量放在接近使用它们的位置,大多数人类程序员都会更好地读取代码.这只是一个风格指南,所以请随意不同意它,但开发人员之间有一个非常明显的共识,即这是"正确的方式".
现在针对一些极端情况:
alloca
在上面的一层上处理.对于那些好奇的人来说,alloca
实现往往具有将堆栈指针向下移动一些任意量的效果.alloca
需要使用函数来以这种或那种方式跟踪此空间,并确保在离开之前向上重新调整堆栈指针.alloca
.Jer*_*odi 22
在C中,我相信所有变量声明都被应用,就好像它们位于函数声明的顶部; 如果你在一个块中声明它们,我认为它只是一个范围界定(我不认为它在C++中是相同的).编译器将对变量执行所有优化,有些甚至可能在更高优化的机器代码中有效消失.然后编译器将决定变量需要多少空间,然后在执行期间创建一个称为变量所在堆栈的空间.
调用函数时,函数使用的所有变量都会被放入堆栈,以及有关被调用函数的信息(即返回地址,参数等).没关系,其中变量被声明,只是它被宣布-它会被分配到堆栈,不管.
声明变量本身并不"昂贵"; 如果它不容易被用作变量,编译器可能会将其作为变量删除.
看一下这个:
当然,所有这些都依赖于实现和系统.
Mar*_*mes 11
是的,它可以成本清晰.如果在某种情况下函数必须根本不执行任何操作(例如,在您的情况下找到全局false),那么将检查放在顶部(上面显示的位置)肯定更容易理解 - 调试和/或记录时必不可少的东西.
Bra*_*orm 11
它最终取决于编译器,但通常所有本地都在函数的开头分配.
但是,分配局部变量的成本非常小,因为它们被放在堆栈上(或者在优化后放入寄存器中).
最好的做法是调整一种懒惰的方法,即只在你真正需要它们时声明它们;)(而不是之前).它带来以下好处:
如果这些变量被声明为尽可能靠近使用地点,则代码更具可读性.
如果你有这个
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
您之后设置声明即使没有优化也可以促进此行为 - 更不用说改进的代码可读性.
无论何时在 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
跳转到清理和返回代码),则不会调用其他指令来“准备”堆栈变量。唯一的区别是存储在edi
和esi
寄存器中的函数参数 a 和 b加载到堆栈中的地址高于第一个示例([rbp-4]
和[rbp - 8]
)。这是因为没有像第一个示例中那样为局部变量“分配”额外的空间。因此,如您所见,添加这些局部变量的唯一“开销”是减法项的更改(即甚至不添加额外的减法运算)。
因此,在您的情况下,简单地声明堆栈变量几乎没有成本。