在C#中,引用数组变量会使程序变慢吗?

25

我有一个整数数组,并且正在循环遍历它们:

for (int i = 0; i < data.Length; i++)
{
  // do a lot of stuff here using data[i]
}

如果我这样做:

for (int i = 0; i < data.Length; i++)
{
  int value = data[i];
  // do a lot of stuff with value instead of data[i]
}

有没有性能上的增益/损失?

据我了解,C/C++数组元素直接访问,即整数的n元素数组具有长度为n * sizeof(int)的连续内存块,并且程序通过类似于*data[i] = *data[0] + (i * sizeof(int))的方式访问元素i。(请原谅我的符号滥用,但你明白我的意思。)

这意味着C/C++应该在引用数组变量方面没有性能上的增益/损失。

C#呢? C#有很多额外的开销,比如data.Length、data.IsSynchronized、data.GetLowerBound()和data.GetEnumerator()。

显然,C#数组不同于C/C++数组。

那么结论是什么?我应该将int value = data[i]存储起来并使用value吗?还是没有性能影响?


5
为什么不试一下,自己去发现呢? - John Saunders
非常微不足道,我认为如果有的话也很少。 - Bala R
1
类似的情况下,我的团队注意到通过将索引值加载到本地变量并引用它来节省了大量时间,而不是一直引用索引值。 - Jeremy Holovacs
当然。局部变量存储在堆栈上!如果它传播到CPU级别3/2/1缓存,它将比访问堆上的数组快得多。 - CoR
5个回答

24

你可以两全其美。在许多情况下,抖动优化器可以轻松确定数组索引访问是否安全,无需进行检查。像您在问题中得到的任何for循环都是这种情况之一,抖动程序知道索引变量的范围。并且知道再次检查它是毫无意义的。

您可以从生成的机器代码中了解到这一点。我会给出一个带注释的示例:

    static void Main(string[] args) {
        int[] array = new int[] { 0, 1, 2, 3 };
        for (int ix = 0; ix < array.Length; ++ix) {
            int value = array[ix];
            Console.WriteLine(value);
        }
    }

Starting at the for loop, ebx has the pointer to the array:

            for (int ix = 0; ix < array.Length; ++ix) {
00000037  xor         esi,esi                       ; ix = 0
00000039  cmp         dword ptr [ebx+4],0           ; array.Length < 0 ?
0000003d  jle         0000005A                      ; skip everything
                int value = array[ix];
0000003f  mov         edi,dword ptr [ebx+esi*4+8]   ; NO BOUNDS CHECK !!!
                Console.WriteLine(value);
00000043  call        6DD5BE38                      ; Console.Out
00000048  mov         ecx,eax                       ; arg = Out
0000004a  mov         edx,edi                       ; arg = value
0000004c  mov         eax,dword ptr [ecx]           ; call WriteLine()
0000004e  call        dword ptr [eax+000000BCh] 
            for (int ix = 0; ix < array.Length; ++ix) {
00000054  inc         esi                           ; ++ix
00000055  cmp         dword ptr [ebx+4],esi         ; array.Length > ix ?
00000058  jg          0000003F                      ; loop

数组索引发生在地址00003f处,ebx为数组指针,esi是索引,8是对象中数组元素的偏移量。请注意,esi值不会再次与数组边界进行检查。这段代码的运行速度与C编译器生成的代码一样快。


1
我认为解决OP的情况很重要。他提出了一个正确的问题,循环很重要。这是低效代码影响快速扩散的地方。核心答案是它确实有效,而不仅仅是“可能优化”或“只是因为”。 - Hans Passant
1
Hmya,抖动优化是为了在常见的代码片段上产生良好的结果。当然它肯定会被打败。我目前没有好的例子,因为当我得出结论“哇,这真的很不错,我现在要使用它”时,我就停止寻找了。 - Hans Passant
1
@Hans:是的,我确实这么想。现在+1,因为你真的花时间拆开了它。 :) - user541686
1
在这种情况下,您将真正感激C#进行数组边界检查。Jitter不会阻止您做错事情,它只有在您做正确的事情时才有所帮助。 - Hans Passant
1
数组元素类型并不重要。 - Hans Passant
显示剩余7条评论

17

是的,由于每次访问数组都需要进行边界检查,因此会有性能损失。

,你很可能不需要担心这个问题。

是的,你可以存储该值并使用它。不是因为性能问题,而是因为这使代码更易读(在我看来)。


顺便说一下,JIT编译器可能会优化掉冗余的检查,因此并不意味着每次调用都会进行检查。无论如何,你可能不必花时间去担心它;只需使用它,如果它成为瓶颈,你可以回头使用unsafe块。


3
每个良好的优化器(至少对于像Java或C#这样的语言是如此,这并不是普遍规律)都会执行公共子表达式消除,从而消除性能损失。在指定的代码中,边界检查无论如何都会被使用。 - Voo
1
我相信JIT编译器会针对像这样的常见循环优化边界检查,以避免出现可能超出范围的异常。 - jakobbotsch

3
您已经两种方式都写了出来。两种方式都运行并测量,然后您就会知道哪种更好。
但我认为您更喜欢使用副本而不是直接操作数组元素,因为这样更容易编写代码,特别是如果您有很多涉及该特定值的操作。

1
编译器只有在能够证明该数组没有被其他线程或循环内调用的任何方法(包括委托)访问时,才能在此执行公共子表达式优化。你最好自己创建本地副本。
但可读性应该是您的主要考虑因素,除非此循环执行大量次数。
所有这些在C和C++中也是正确的--索引到一个数组将比访问局部变量慢。
顺便说一句,你提出的优化不好:`value`是一个关键字,请选择一个不同的变量名。

谢谢,是的,我的例子只是为了说明目的 - 我通常会使用像“val”或“num”这样更短的东西来方便。 - Ozzah

0

不太确定,但如果您要多次使用它,存储该值可能不会有害。您也可以使用foreach语句 :)


据我所记,foreach 比 for 循环慢,并且有一个限制,即您无法修改正在处理的数组的大小。例如,foreach(object o in objectList){if(condition(o))objectList.Remove(o);} 将会抛出异常。 - Ozzah

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