使用foreach循环有什么特殊情况可以消除边界检查?

5

在foreach/for循环中,有一种特殊情况可以消除边界检查。这个边界检查是指数组边界检查。


一个额外的标签 for 可能是个好主意,因为答案中也包含了对 for 循环优化的参考。 - Christian Klauser
5个回答

10
标准
for(int i = 0; i < array.Length; i++) {
    ...
}

循环是允许JIT安全地删除数组边界检查的工具之一(无论索引是否在[0..length-1]之间)。

使用foreach循环遍历数组等价于使用标准的for循环遍历数组。

编辑: 正如Robert Jeppesen指出的那样:

如果该数组是本地的,则此操作将被优化。 如果该数组可以从其他位置访问,则仍然会执行边界检查。 参考:CLR中的数组边界检查消除

谢谢!我自己不知道。


是的,没错。在数组上进行foreach循环时会发出一个'for'循环。 - leppie
1
我猜测,优化仅适用于array[i]的确切出现。因此,array[i+1]很可能会导致数组边界检查。 - Christian Klauser
2
如果数组是本地的,这将被优化。如果数组可以从其他位置访问,则仍将执行边界检查。参考:http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspx - Robert Jeppesen
一个私有的成员数组算作“局部”的吗,因为它不能从其他位置访问? - BlueRaja - Danny Pflughoeft
@BlueRaja-DannyPflughoeft 所链接的文章表示“不行”,因为该字段仍可从您的循环中直接或间接调用的其他方法(或来自不同线程)中访问。也许 readonly 可以帮助解决问题。但您始终可以将数组引用复制到本地变量中。对于边界检查,只需要本地化引用即可(分配后数组大小不会改变)。 - Christian Klauser
显示剩余2条评论

7

SealedSun是正确的。不要像在C++中那样进行优化。JIT非常聪明,可以为您做正确的事情。您始终可以以不同的方式编写循环,然后检查IL代码。

        static void Main(string[] args)
        {
            int[] array = new int[100];
00000000  push        edi  
00000001  push        esi  
00000002  push        eax  
00000003  xor         eax,eax 
00000005  mov         dword ptr [esp],eax 
00000008  mov         edx,64h 
0000000d  mov         ecx,79174292h 
00000012  call        49E73198 
00000017  mov         esi,eax 
            int sum = 0;
00000019  xor         edx,edx 
0000001b  mov         dword ptr [esp],edx 
            for(int index = 0; index < array.Length; index++)
0000001e  mov         edi,dword ptr [esi+4] 
00000021  test        edi,edi 
00000023  jle         00000033 
            {
                sum += array[index];
00000025  mov         eax,dword ptr [esi+edx*4+8] 
00000029  add         dword ptr [esp],eax 
            for(int index = 0; index < array.Length; index++)
0000002c  add         edx,1 
0000002f  cmp         edi,edx 
00000031  jg          00000025 
            }

            Console.WriteLine(sum.ToString());
00000033  mov         esi,dword ptr [esp] 
00000036  call        493765F8 
0000003b  push        eax  
0000003c  mov         ecx,esi 
0000003e  xor         edx,edx 
00000040  call        49E83A8B 
00000045  mov         edi,eax 
00000047  mov         edx,88h 
0000004c  mov         ecx,1 
00000051  call        49E731B0 
00000056  mov         esi,eax 
00000058  cmp         dword ptr [esi+70h],0 
0000005c  jne         00000068 
0000005e  mov         ecx,1 
00000063  call        4936344C 
00000068  mov         ecx,dword ptr [esi+70h] 
0000006b  mov         edx,edi 
0000006d  mov         eax,dword ptr [ecx] 
0000006f  call        dword ptr [eax+000000D8h] 
00000075  pop         ecx  
        }
00000076  pop         esi  
00000077  pop         edi  
00000078  ret              

现在,如果按照C++的方式优化代码,您会得到以下结果:
        static void Main(string[] args)
        {
            int[] array = new int[100];
00000000  push        edi  
00000001  push        esi  
00000002  push        ebx  
00000003  push        eax  
00000004  xor         eax,eax 
00000006  mov         dword ptr [esp],eax 
00000009  mov         edx,64h 
0000000e  mov         ecx,79174292h 
00000013  call        49E73198 
00000018  mov         esi,eax 
            int sum = 0;
0000001a  xor         edx,edx 
0000001c  mov         dword ptr [esp],edx 

            int length = array.Length;
0000001f  mov         ebx,dword ptr [esi+4] 
            for (int index = 0; index < length; index++)
00000022  test        ebx,ebx 
00000024  jle         0000003B 
00000026  mov         edi,dword ptr [esi+4] 
            {
                sum += array[index];
00000029  cmp         edx,edi                  <-- HERE
0000002b  jae         00000082                 <-- HERE
0000002d  mov         eax,dword ptr [esi+edx*4+8] 
00000031  add         dword ptr [esp],eax 
            for (int index = 0; index < length; index++)
00000034  add         edx,1 
00000037  cmp         edx,ebx 
00000039  jl          00000029 
            }

            Console.WriteLine(sum.ToString());
0000003b  mov         esi,dword ptr [esp] 
0000003e  call        493765F8 
00000043  push        eax  
00000044  mov         ecx,esi 
00000046  xor         edx,edx 
00000048  call        49E83A8B 
0000004d  mov         edi,eax 
0000004f  mov         edx,88h 
00000054  mov         ecx,1 
00000059  call        49E731B0 
0000005e  mov         esi,eax 
00000060  cmp         dword ptr [esi+70h],0 
00000064  jne         00000070 
00000066  mov         ecx,1 
0000006b  call        4936344C 
00000070  mov         ecx,dword ptr [esi+70h] 
00000073  mov         edx,edi 
00000075  mov         eax,dword ptr [ecx] 
00000077  call        dword ptr [eax+000000D8h] 
0000007d  pop         ecx  
        }
0000007e  pop         ebx  
0000007f  pop         esi  
00000080  pop         edi  
00000081  ret              
00000082  call        4A12746B 
00000087  int         3    

顺便说一下 - 这里与foreach语句相同:

        static void Main(string[] args)
        {
            int[] array = new int[100];
00000000  push        edi  
00000001  push        esi  
00000002  push        eax  
00000003  xor         eax,eax 
00000005  mov         dword ptr [esp],eax 
00000008  mov         edx,64h 
0000000d  mov         ecx,79174292h 
00000012  call        49E73198 
00000017  mov         esi,eax 
            int sum = 0;
00000019  xor         edx,edx 
0000001b  mov         dword ptr [esp],edx 
            for(int index = 0; index < array.Length; index++)
0000001e  mov         edi,dword ptr [esi+4] 
00000021  test        edi,edi 
00000023  jle         00000033 
            {
                sum += array[index];
00000025  mov         eax,dword ptr [esi+edx*4+8] 
00000029  add         dword ptr [esp],eax 
            for(int index = 0; index < array.Length; index++)
0000002c  add         edx,1 
0000002f  cmp         edi,edx 
00000031  jg          00000025 
            }

            Console.WriteLine(sum.ToString());
00000033  mov         esi,dword ptr [esp] 
00000036  call        493765F8 
0000003b  push        eax  
0000003c  mov         ecx,esi 
0000003e  xor         edx,edx 
00000040  call        49E83A8B 
00000045  mov         edi,eax 
00000047  mov         edx,88h 
0000004c  mov         ecx,1 
00000051  call        49E731B0 
00000056  mov         esi,eax 
00000058  cmp         dword ptr [esi+70h],0 
0000005c  jne         00000068 
0000005e  mov         ecx,1 
00000063  call        4936344C 
00000068  mov         ecx,dword ptr [esi+70h] 
0000006b  mov         edx,edi 
0000006d  mov         eax,dword ptr [ecx] 
0000006f  call        dword ptr [eax+000000D8h] 
00000075  pop         ecx  
        }
00000076  pop         esi  
00000077  pop         edi  
00000078  ret              

不要在没有数据的情况下尝试优化代码。正如您所看到的,如果您不阻碍JIT,它将为您做很多工作。在进行优化之前,请使用分析器。一定要使用分析器。


呵呵,“一张图片胜过千言万语”^^ - Christian Klauser
+1 用于证明,“不要妨碍JIT”,以及“不要过早优化”。 - Lucas
@David:这些内容适用于列表或IList吗? - Lucas

6
请参考以下内容:

详情请查看:

http://codebetter.com/blogs/david.hayden/archive/2005/02/27/56104.aspx

如果您使用for循环,并且明确引用IList.Count或Array.Length,则JIT会捕获该操作并跳过边界检查。这比预先计算列表长度更快。

我相信,在列表或数组上进行foreach循环也会在内部执行相同的操作。


如果您引用IList.Count但出现偏移一个错误,并最终访问超出范围的内容会发生什么?或者它比那更聪明吗? - rjh
1
只有在for循环中才会进行优化。如果你在for块内引用list.Count,那么边界检查将不会被优化。例如:for (int i=0;i<list.Count;++i) // 边界检查被跳过 { list[i-1]; // 这部分不会被优化,并且会进行边界检查 } - Reed Copsey
我认为在“for”循环中仍然会检查列表边界,因为其计数可以随时更改(如果集合更改,“foreach”会抛出异常)。 - Lucas
@Lucas:如果循环内部没有操作集合,JIT 会进行优化。有很多文章展示了这一点,但你也可以自己测试一下。 - Reed Copsey
@Reed:"如果循环内没有触及集合",但如果在循环外触及呢?该列表可以作为参数传入,并且另一个线程可以添加(Add())或删除(Remove())项目。JIT不能做任何假设或优化,因为列表计数可能会改变(不像数组)。 - Lucas
@Reed:谢谢,但我找到的每篇文章都是关于优化数组边界检查,而不是列表。List的索引属性Item/get_Item()在返回this._list[index]之前会检查索引,因此即使内部数组访问被优化,仍然存在显式的边界检查。而且,数组字段根本没有进行边界检查优化,只有本地或传入的数组才有。除非当然List获得JIT编译器的特殊处理,即使这样也不能应用于任何IList。 - Lucas

0
一个foreach循环使用枚举器,它是一个处理循环的类或结构。枚举器有一个Current属性,返回集合中当前的项。这消除了使用索引访问集合中的项的需要,因此不需要额外的步骤来获取项,包括边界检查。

自从框架1.1以来,“foreach”循环数组变成了简单的“for”循环,以避免IEnumerator开销(创建枚举器实例、方法调用、处理),因此边界检查仍然可以被优化掉。 - Lucas
“bounds checking is not needed”这句话意味着在枚举器的MoveNext()方法内部仍然需要进行边界检查,例如当List<T>通过索引访问其内部数组时。 - Lucas

-3

什么?我不确定在C#中是否可能消除边界检查。如果你想要非托管代码,那就使用:

int[] array;
fixed (int * i = array)
{
 while (i++)
  Console.WriteLine("{0}", *i);
}

例如 - 它不检查边界,而且会死得很惨。:-)

抱歉,我没有意识到这可能与JIT优化有关。 - nothrow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接