是否应该将数组长度存储到C#中的局部变量中?

我注意到人们经常使用这样的构造:

var length = array.Length; for (int i = 0; i < length; i++) {    //do smth } 

他们认为每次迭代都调用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):
没有变量()
; int sum = 0;
埃迪
; int i = 0;
埃西
; int [] localRefToArray = this.array;
mov edx dword ptr [ ecx + 4 ]
; int arrayLength = localRefToArray.Length;
mov ecx dword ptr [ edx + 4 ]
;如果(arrayLength == 0)返回总和;
测试版
出口
; int arrayLength2 = localRefToArray.Length;
mov eax dword ptr [ edx + 4 ]
;如果(i> = arrayLength2)
; 抛出新的IndexOutOfRangeException();
循环
电子邮件
杰056e2d31
; sum + = localRefToArray [i];
添加 edi dword ptr [ edx + esi * 4 + 8 ]
;我++;
公司
; if(i <arrayLength)转到循环
cmp文件
jg 循环
;返回总和
退出
动画
WithVariable()
; int sum = 0;
埃西
; int [] localRefToArray = this.array;
mov edx dword ptr [ ecx + 4 ]
; int arrayLength = localRefToArray.Length;
mov edi dword ptr [ edx + 4 ]
; int i = 0;
异或
;如果(arrayLength == 0)返回总和;
测试 edi edi
出口
; int arrayLength2 = localRefToArray.Length;
mov ecx dword ptr [ edx + 4 ]
;如果(i> = arrayLength2)
; 抛出新的IndexOutOfRangeException();
循环
电子邮件
05902d31
; sum + = localRefToArray [i];
添加 esi dword ptr [ edx + eax * 4 + 8 ]
;我++;
公司
; if(i <arrayLength)转到循环
cmp eax edi
jl 循环
;返回总和
退出
动画

比较中:


值得注意的是,它们具有完全相同数量的汇编程序指令-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 sum = 0 ;
埃迪
int i = 0 ;
埃西
int [ ] localRefToArray = this 数组;
mov edx dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray 长度;
mov ebx dword ptr [ edx + 4 ]
如果 arrayLength == 0 返回和;
测试 ebx
出口
int arrayLength2 = localRefToArray 长度;
mov ecx dword ptr [ edx + 4 ]
如果 i> = arrayLength2
抛出新的IndexOutOfRangeException ;
循环
cmp esi ecx
杰伊05562d39
int =数组[ i ]
mov eax dword ptr [ edx + esi * 4 + 8 ]
t + =和;
添加 eax edi
t + = arrayLength ;
添加 eax ebx
总和= t ;
动画
++ ;
公司
如果 i <arrayLength 转到循环
cmp ebx文件
jg 循环
收益总额;
退出
动画
WithVariable()
int sum = 0 ;
埃西
int [ ] localRefToArray = this 数组;
mov edx dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray 长度;
mov ebx dword ptr [ edx + 4 ]
int i = 0 ;
异或
if arrayLength == 0 返回和;)
测试 ebx
出口
int arrayLength2 = localRefToArray 长度;
mov edi dword ptr [ edx + 4 ]
如果 i> = arrayLength2
抛出新的IndexOutOfRangeException ;
循环
cmp ecx edi
杰伊04b12d39
int =数组[ i ]
mov eax dword ptr [ edx + ecx * 4 + 8 ]
t + =和;
添加 eax esi
t + = arrayLength ;
添加 eax ebx
总和= t ;
动画
++ ;
ecx
如果 i <arrayLength 转到循环
电子 表格
jl 循环
收益总额;
退出
动画

比较中:


再一次,指令的数量以及(几乎)指令本身都是相同的。 唯一的区别是变量初始化的顺序和循环继续的检查条件。 您可以注意到,在计算总和时,仅考虑数组的第一个长度。 显然,这是:
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]; 
方法
ItemsCount
均值
失误
标准差
中位数
比例
比率SD
佛瑞奇
1,000,000
1.401毫秒
0.2691毫秒
0.7935毫秒
1.694毫秒
1.00
0.00
对于
1,000,000
1.586毫秒
0.3204毫秒
0.9447毫秒
1.740毫秒
1.23
0.65
而对于
 sum+=array[i] + array.Length; 
方法
ItemsCount
均值
失误
标准差
中位数
比例
比率SD
佛瑞奇
1,000,000
1.703毫秒
0.3010毫秒
0.8874毫秒
1.726毫秒
1.00
0.00
对于
1,000,000
1.715毫秒
0.2859毫秒
0.8430毫秒
1.956毫秒
1.13
0.56

ForEach遍历数组要比forE快得多。 怎么了 为了找出答案,我们需要在JIT之后比较代码:

所有三个foreach选项的比较


让我们看看ForEachWithoutLength。 仅请求一次数组长度,并且不检查数组边界。 之所以会发生这种情况,是因为ForEach循环首先限制在循环内更改集合,而第二个循环永远不会超出集合之外。 因此,JIT可以消除检查数组的边界。

现在,让我们仔细看看ForEachWithLengthWIthoutLocalVariable。 只有一个奇怪的部分,sum + = length不是发生在先前保存的局部变量arrayLength上,而是发生在应用程序每次从内存中请求的新部分。 这就是说,将有N + 1个存储请求用于数组长度,其中N是数组长度。

现在我们来看看ForEachWithLengthWithLocalVariable。 除了处理数组长度外,其中的代码与上一个示例完全相同。 编译器再次生成局部变量数组 事实证明,此方法仅从内存请求两次数组长度。 在现实世界中,很难注意到这种差异。

在所有情况下,汇编程序代码都非常简单,因为方法本身很简单。 如果方法具有更多参数,则必须与堆栈配合使用,变量可能会在寄存器之外获取存储,将进行更多检查,但主要逻辑将保持不变:仅针对数组长度引入局部变量对于使代码更具可读性很有用。 事实证明, Foreach遍历数组的速度通常比For快。

Source: https://habr.com/ru/post/zh-CN454582/


All Articles