我注意到人们经常使用这样的构造:
var length = array.Length; for (int i = 0; i < length; i++) {
他们认为每次迭代都调用Array.Length将使CLR花更多时间执行代码。 为了避免这种情况,他们将长度值存储在局部变量中。
让我们找出(一劳永逸!)这是否可行,或者使用临时变量会浪费时间。
首先,让我们检查以下C#方法:
public int WithoutVariable() { int sum = 0; for (int i = 0; i < array.Length; i++) { sum += array[i]; } return sum; } public int WithVariable() { int sum = 0; int length = array.Length; for (int i = 0; i < length; i++) { sum += array[i]; } return sum; }
这是JIT编译器处理后的外观(对于LegacyJIT-x86下的.NET Framework 4.7.2):
值得注意的是,它们具有完全相同数量的汇编程序指令-15。甚至这些指令的逻辑几乎相同。 在初始化变量和比较循环是否应该继续的顺序上有细微的差别。 我们可以注意到,在这两种情况下,数组长度在循环之前都会被注册两次:
- 检查0(arrayLength)
- 放入用于检查循环条件的临时变量(arrayLength2)。
事实证明,这两种方法都可以编译为完全相同的代码,但是第一种方法的编写速度更快,即使在执行时间方面没有任何好处。
上面的汇编代码使我产生了一些想法,因此我决定检查另外两种方法:
public int WithoutVariable() { int sum = 0; for(int i = 0; i < array.Length; i++) { sum += array[i] + array.Length; } return sum; } public int WithVariable() { int sum = 0; int length = array.Length; for(int i = 0; i < length; i++) { sum += array[i] + length; } return sum; }
现在将当前元素和数组长度相加,但是在第一种情况下,每次都请求数组长度,在第二种情况下,将其保存一次到局部变量中。 让我们看一下这些方法的汇编代码:
再一次,指令的数量以及(几乎)指令本身都是相同的。 唯一的区别是变量初始化的顺序和循环继续的检查条件。 您可以注意到,在计算总和时,仅考虑数组的第一个长度。 显然,这是:
int arrayLength2 = localRefToArray 。 长度;
mov edi , dword ptr [ edx + 4 ]
如果( i> = arrayLength2 )抛出新的IndexOutOfRangeException ( ) ;
cmp ecx edi
杰伊04b12d39
在所有四种方法中,都是内联数组边界检查,并且对数组的每个元素执行该检查。
我们已经可以得出第一个结论:使用额外的变量来尝试加快周期是浪费时间,因为无论如何编译器都会为您完成。 将长度数组存储到变量中的唯一原因是使代码更具可读性。
ForEach完全是另一种情况。 请考虑以下三种方法:
public int ForEachWithoutLength() { int sum = 0; foreach (int i in array) { sum += i; } return sum; } public int ForEachWithLengthWithoutLocalVariable() { int sum = 0; foreach (int i in array) { sum += i + array.Length; } return sum; } public int ForEachWithLengthWithLocalVariable() { int sum = 0; int length = array.Length; foreach (int i in array) { sum += i + length; } return sum; }
这是JIT之后的代码:
ForEachWithoutLength(); int sum = 0;
埃西
; int [] localRefToArray = this.array;
mov ecx , dword ptr [ ecx + 4 ]
; int i = 0;
异或
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ ecx + 4 ]
;如果(arrayLength == 0)转到出口;
测试 edi edi
出口
; int t =数组[i];
循环 :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
;总和= i;
添加 esi , eax
;我++;
Inc edx
; if(i <arrayLength)转到循环
cmp edi edx
jg 循环
;返回总和
退出:
动画
ForEachWithLengthWithoutLocalVariable(); int sum = 0;
埃西
; int [] localRefToArray = this.array;
mov ecx , dword ptr [ ecx + 4 ]
; int i = 0;
异或
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ ecx + 4 ]
;如果(arrayLength == 0)转到出口
测试 edi edi
出口
; int t =数组[i];
循环 :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
;总和= i;
添加 esi , eax
; sum + = localRefToArray.Length;
添加 esi , dword ptr [ ecx + 4 ]
;我++;
Inc edx
; if(i <arrayLength)转到循环
cmp edi edx
jg 循环
;返回总和
退出:
动画
ForEachWithLengthWithLocalVariable(); int sum = 0;
埃西
; int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
; int length = localRefToArray.Length;
mov ebx , dword ptr [ edx + 4 ]
; int i = 0;
异或
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ edx + 4 ]
;如果(arrayLength == 0)转到出口;
测试 edi edi
出口
; int t =数组[i];
循环 :
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
;总和= i;
添加 esi , eax
;总和=长度;
添加 esi , ebx
;我++;
ecx
; if(i <arrayLength)转到循环
cmp edi ecx
jg 循环
;返回总和
退出:
动画
首先想到的是,与
for循环相比,它所需的汇编指令更少(例如,对于简单元素求和,
foreach中需要12条指令,而
for中则需要15条指令)。
总体而言,这是一百万个元素数组的
for vs foreach基准测试结果:
sum+=array[i];
而对于
sum+=array[i] + array.Length;
ForEach遍历数组要比
forE快得多。 怎么了 为了找出答案,我们需要在JIT之后比较代码:
让我们看看ForEachWithoutLength。 仅请求一次数组长度,并且不检查数组边界。 之所以会发生这种情况,是因为ForEach循环首先限制在循环内更改集合,而第二个循环永远不会超出集合之外。 因此,JIT可以消除检查数组的边界。
现在,让我们仔细看看ForEachWithLengthWIthoutLocalVariable。 只有一个奇怪的部分,sum + = length不是发生在先前保存的局部变量arrayLength上,而是发生在应用程序每次从内存中请求的新部分。 这就是说,将有N + 1个存储请求用于数组长度,其中N是数组长度。
现在我们来看看ForEachWithLengthWithLocalVariable。 除了处理数组长度外,其中的代码与上一个示例完全相同。 编译器再次生成局部变量数组 事实证明,此方法仅从内存请求两次数组长度。 在现实世界中,很难注意到这种差异。
在所有情况下,汇编程序代码都非常简单,因为方法本身很简单。 如果方法具有更多参数,则必须与堆栈配合使用,变量可能会在寄存器之外获取存储,将进行更多检查,但主要逻辑将保持不变:仅针对数组长度引入局部变量对于使代码更具可读性很有用。 事实证明,
Foreach遍历数组的速度通常比
For快。