在循环之前或循环中声明变量之间的区别?
-
03-07-2019 - |
题
我一直想知道,一般来说,在循环之前声明一个一次性变量(而不是在循环内重复声明)是否会产生任何(性能)差异?A (完全没有意义) Java 中的示例:
A) 循环前声明:
double intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
b) 循环内(重复)声明:
for(int i=0; i < 1000; i++){
double intermediateResult = i;
System.out.println(intermediateResult);
}
哪一个更好, A 或者 乙?
我怀疑重复的变量声明(例如 乙) 产生更多开销 理论上, ,但是编译器足够聪明,所以这并不重要。例子 乙 具有更紧凑并将变量的范围限制在其使用位置的优点。尽管如此,我还是倾向于根据示例进行编码 A.
编辑: 我对 Java 案例特别感兴趣。
解决方案
哪个更好, A 或者 乙?
从性能的角度来看,您必须对其进行衡量。(在我看来,如果你能衡量差异,那么编译器就不是很好)。
从维护角度来看, 乙 更好。在同一位置、尽可能小的范围内声明和初始化变量。不要在声明和初始化之间留下间隙,也不要污染不需要的命名空间。
其他提示
好吧,我运行了你的 A 和 B 示例各 20 次,循环 1 亿次。(JVM - 1.5.0)
A:平均执行时间:.074秒
乙:平均执行时间:.067秒
令我惊讶的是,B 稍微快一些。现在计算机的速度越来越快,很难说是否可以准确地测量它。我也会用 A 方式对其进行编码,但我想说这并不重要。
这取决于语言和具体用途。例如,在 C# 1 中,这没有什么区别。在 C# 2 中,如果局部变量由匿名方法(或 C# 3 中的 lambda 表达式)捕获,则可以产生非常显着的差异。
例子:
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
List<Action> actions = new List<Action>();
int outer;
for (int i=0; i < 10; i++)
{
outer = i;
int inner = i;
actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
}
foreach (Action action in actions)
{
action();
}
}
}
输出:
Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9
不同之处在于所有的动作都捕获相同的内容 outer
变量,但每个都有自己独立的 inner
多变的。
以下是我在.NET中编写和编译的内容。
double r0;
for (int i = 0; i < 1000; i++) {
r0 = i*i;
Console.WriteLine(r0);
}
for (int j = 0; j < 1000; j++) {
double r1 = j*j;
Console.WriteLine(r1);
}
这就是我从中得到的 .NET反射器 什么时候 化学工业协会 被渲染回代码。
for (int i = 0; i < 0x3e8; i++)
{
double r0 = i * i;
Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
double r1 = j * j;
Console.WriteLine(r1);
}
所以编译后两者看起来完全一样。在托管语言中,代码被转换为 CL/字节代码,并在执行时转换为机器语言。因此,在机器语言中,甚至可能无法在堆栈上创建双精度型。它可能只是一个寄存器,因为代码反映它是一个临时变量 WriteLine
功能。仅针对循环就有一整套优化规则。因此,普通人不应该担心这一点,尤其是在托管语言中。在某些情况下,您可以优化管理代码,例如,如果您必须仅使用连接大量字符串 string a; a+=anotherstring[i]
与使用 StringBuilder
. 。两者之间的性能差异非常大。在很多这样的情况下,编译器无法优化您的代码,因为它无法弄清楚更大范围内的意图。但它几乎可以为您优化基本的东西。
这是 VB.NET 中的一个陷阱。Visual Basic 结果不会重新初始化此示例中的变量:
For i as Integer = 1 to 100
Dim j as Integer
Console.WriteLine(j)
j = i
Next
' Output: 0 1 2 3 4...
这将第一次打印 0(Visual Basic 变量在声明时有默认值!)但是 i
此后每次。
如果您添加一个 = 0
, 不过,您会得到您所期望的:
For i as Integer = 1 to 100
Dim j as Integer = 0
Console.WriteLine(j)
j = i
Next
'Output: 0 0 0 0 0...
我做了一个简单的测试:
int b;
for (int i = 0; i < 10; i++) {
b = i;
}
与
for (int i = 0; i < 10; i++) {
int b = i;
}
我用 gcc - 5.2.0 编译了这些代码。然后我拆除了这两个代码的主(),这就是结果:
1º:
0x00000000004004b6 <+0>: push rbp
0x00000000004004b7 <+1>: mov rbp,rsp
0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0
0x00000000004004c1 <+11>: jmp 0x4004cd <main+23>
0x00000000004004c3 <+13>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004004c6 <+16>: mov DWORD PTR [rbp-0x8],eax
0x00000000004004c9 <+19>: add DWORD PTR [rbp-0x4],0x1
0x00000000004004cd <+23>: cmp DWORD PTR [rbp-0x4],0x9
0x00000000004004d1 <+27>: jle 0x4004c3 <main+13>
0x00000000004004d3 <+29>: mov eax,0x0
0x00000000004004d8 <+34>: pop rbp
0x00000000004004d9 <+35>: ret
与
2º
0x00000000004004b6 <+0>: push rbp
0x00000000004004b7 <+1>: mov rbp,rsp
0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0
0x00000000004004c1 <+11>: jmp 0x4004cd <main+23>
0x00000000004004c3 <+13>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004004c6 <+16>: mov DWORD PTR [rbp-0x8],eax
0x00000000004004c9 <+19>: add DWORD PTR [rbp-0x4],0x1
0x00000000004004cd <+23>: cmp DWORD PTR [rbp-0x4],0x9
0x00000000004004d1 <+27>: jle 0x4004c3 <main+13>
0x00000000004004d3 <+29>: mov eax,0x0
0x00000000004004d8 <+34>: pop rbp
0x00000000004004d9 <+35>: ret
这与 asm 结果完全相同。这不是证明这两个代码产生相同的结果吗?
我总是使用 A (而不是依赖编译器)并且也可能重写为:
for(int i=0, double intermediateResult=0; i<1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
这还是限制了 intermediateResult
到循环的范围,但不会在每次迭代期间重新声明。
它取决于语言 - IIRC C# 对此进行了优化,因此没有任何区别,但 JavaScript(例如)每次都会执行整个内存分配工作。
我认为b是更好的结构。在 a 中,循环完成后,intermediateResult 的最后一个值仍然存在。
编辑:这与值类型没有太大区别,但引用类型可能有点重要。就我个人而言,我喜欢尽快取消引用变量以进行清理,b 会为您做到这一点,
我怀疑一些编译器可以将两者优化为相同的代码,但肯定不是全部。所以我想说你最好选择前者。后者的唯一原因是如果您想确保使用声明的变量 仅有的 在你的循环内。
作为一般规则,我在最内部可能的范围内声明变量。因此,如果您不在循环之外使用 middleResult,那么我会选择 B。
一位同事更喜欢第一种形式,告诉他这是一种优化,更喜欢重用声明。
我更喜欢第二个(并尝试说服我的同事!;-)),读过:
- 它将变量的范围缩小到需要的地方,这是一件好事。
- Java 进行了足够的优化,不会造成性能上的显着差异。IIRC,也许第二种形式更快。
不管怎样,它属于依赖编译器和/或 JVM 质量的过早优化的范畴。
如果您在 lambda 等中使用变量,则 C# 中存在差异。但一般来说,假设变量仅在循环内使用,编译器基本上会做同样的事情。
鉴于它们基本相同:请注意,版本 b 使读者更加明显地意识到该变量在循环之后不能使用。此外, 版本 b 更容易重构。在版本a中将循环体提取到它自己的方法中更加困难。 此外,版本 b 向您保证这种重构不会产生任何副作用。
因此,版本 a 让我烦恼不已,因为它没有任何好处,而且使推理代码变得更加困难......
好吧,你总是可以为此制定一个范围:
{ //Or if(true) if the language doesn't support making scopes like this
double intermediateResult;
for (int i=0; i<1000; i++) {
intermediateResult = i;
System.out.println(intermediateResult);
}
}
这样你只需声明一次变量,当你离开循环时它就会消失。
我一直认为,如果您在循环内声明变量,那么您就是在浪费内存。如果你有这样的事情:
for(;;) {
Object o = new Object();
}
那么不仅需要为每次迭代创建对象,而且需要为每个对象分配一个新的引用。看起来,如果垃圾收集器很慢,那么您将有一堆需要清理的悬空引用。
但是,如果你有这个:
Object o;
for(;;) {
o = new Object();
}
然后,您只需创建一个引用并每次为其分配一个新对象。当然,它可能需要更长的时间才能超出范围,但这样就只有一个悬空引用需要处理。
我认为这取决于编译器,很难给出一般答案。
我的做法如下:
如果变量类型很简单 (整数、双精度、...) 我更喜欢变体 乙 (里面)。
原因: 减少变量的范围。如果变量类型不简单 (一些
class
或者struct
) 我更喜欢变体 A (外部)。
原因: 减少ctor-dtor 调用的数量。
从性能角度来看,室外要好得多。
public static void outside() {
double intermediateResult;
for(int i=0; i < Integer.MAX_VALUE; i++){
intermediateResult = i;
}
}
public static void inside() {
for(int i=0; i < Integer.MAX_VALUE; i++){
double intermediateResult = i;
}
}
我分别执行了这两个函数 10 亿次。Outside() 花费了 65 毫秒。inside() 花了 1.5 秒。
A) 比 B) 更安全......想象一下,如果您在循环中初始化结构而不是“int”或“float”,那么会怎样?
喜欢
typedef struct loop_example{
JXTZ hi; // where JXTZ could be another type...say closed source lib
// you include in Makefile
}loop_example_struct;
//then....
int j = 0; // declare here or face c99 error if in loop - depends on compiler setting
for ( ;j++; )
{
loop_example loop_object; // guess the result in memory heap?
}
您肯定会面临内存泄漏的问题!因此,我相信“A”是更安全的选择,而“B”很容易受到内存积累的影响,尤其是在近距离源库工作时。您可以使用 Linux 上的“Valgrind”工具特别是子工具“Helgrind”进行检查。
这是一个有趣的问题。根据我的经验,当您就代码争论这个问题时,需要考虑一个终极问题:
变量需要是全局的有什么原因吗?
与在本地多次声明变量相比,只在全局范围内声明一次变量是有意义的,因为这样可以更好地组织代码并且需要更少的代码行。但是,如果它只需要在一个方法中本地声明,我会在该方法中初始化它,这样很明显该变量与该方法完全相关。如果您选择后一个选项,请小心不要在初始化该变量的方法之外调用该变量 - 您的代码将不知道您在说什么,并且会报告错误。
另外,作为旁注,不要在不同方法之间重复局部变量名称,即使它们的用途几乎相同;它只是变得令人困惑。
如果有人感兴趣的话,我用 Node 4.0.0 测试了 JS。在 1000 次试验(每次试验 1 亿次循环迭代)中,在循环外部声明平均性能提高约 0.5 毫秒。所以我想说,继续以最具可读性/可维护性的方式编写它,即 B,我认为。我会把我的代码放在小提琴中,但我使用了现在的性能 Node 模块。这是代码:
var now = require("../node_modules/performance-now")
// declare vars inside loop
function varInside(){
for(var i = 0; i < 100000000; i++){
var temp = i;
var temp2 = i + 1;
var temp3 = i + 2;
}
}
// declare vars outside loop
function varOutside(){
var temp;
var temp2;
var temp3;
for(var i = 0; i < 100000000; i++){
temp = i
temp2 = i + 1
temp3 = i + 2
}
}
// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;
// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
var start = now()
varInside()
var end = now()
insideAvg = (insideAvg + (end-start)) / 2
}
// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
var start = now()
varOutside()
var end = now()
outsideAvg = (outsideAvg + (end-start)) / 2
}
console.log('declared inside loop', insideAvg)
console.log('declared outside loop', outsideAvg)
这是更好的形式
double intermediateResult;
int i = byte.MinValue;
for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}
1)用这种方式声明一次两个变量,而不是每次for循环。2)这个作业比所有其他选项都更胖。3) 因此,最佳实践规则是迭代之外的任何声明。
在 Go 中尝试了同样的事情,并使用比较了编译器输出 go tool compile -S
与 1.9.4 一起
根据汇编器输出,零差异。
很长一段时间我都有同样的问题。所以我测试了一段更简单的代码。
结论: 为了 此类情况 有 不 性能差异。
外循环情况
int intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i+2;
System.out.println(intermediateResult);
}
内循环外壳
for(int i=0; i < 1000; i++){
int intermediateResult = i+2;
System.out.println(intermediateResult);
}
我检查了 IntelliJ 反编译器上的编译文件,对于这两种情况,我得到了 相同的 Test.class
for(int i = 0; i < 1000; ++i) {
int intermediateResult = i + 2;
System.out.println(intermediateResult);
}
我还使用此中给出的方法反汇编了这两种情况的代码 回答. 。我将仅显示与答案相关的部分
外循环情况
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_2
2: iload_2
3: sipush 1000
6: if_icmpge 26
9: iload_2
10: iconst_2
11: iadd
12: istore_1
13: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_1
17: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
20: iinc 2, 1
23: goto 2
26: return
LocalVariableTable:
Start Length Slot Name Signature
13 13 1 intermediateResult I
2 24 2 i I
0 27 0 args [Ljava/lang/String;
内循环外壳
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: sipush 1000
6: if_icmpge 26
9: iload_1
10: iconst_2
11: iadd
12: istore_2
13: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_2
17: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
20: iinc 1, 1
23: goto 2
26: return
LocalVariableTable:
Start Length Slot Name Signature
13 7 2 intermediateResult I
2 24 1 i I
0 27 0 args [Ljava/lang/String;
如果你仔细观察的话,只有 Slot
分配给 i
和 intermediateResult
在 LocalVariableTable
被交换为它们出现顺序的乘积。插槽中的相同差异也反映在其他代码行中。
- 没有执行额外的操作
intermediateResult
在这两种情况下仍然是局部变量,因此访问时间没有差异。
奖金
编译器做了大量的优化,看看在这种情况下会发生什么。
零工作案例
for(int i=0; i < 1000; i++){
int intermediateResult = i;
System.out.println(intermediateResult);
}
零工反编译
for(int i = 0; i < 1000; ++i) {
System.out.println(i);
}
即使我知道我的编译器足够智能,我也不会喜欢依赖它,并且会使用 a) 变体。
仅当您迫切需要使 b) 变体对我有意义时 中间结果 在循环体之后不可用。但无论如何,我无法想象这种绝望的情况......
编辑: 乔恩·斯基特 提出了一个非常好的观点,表明循环内的变量声明可以产生实际的语义差异。