在 C# 中,引用数组变量会更慢吗?

Posted

技术标签:

【中文标题】在 C# 中,引用数组变量会更慢吗?【英文标题】:In C#, Is it slower to reference an array variable? 【发布时间】:2011-07-31 07:34:58 【问题描述】:

我有一个整数数组,我正在循环它们:

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) 的连续内存块,而程序访问元素 i 通过执行 *data 之类的操作[i] = *data[0] + (i * sizeof(int))。 (请原谅我滥用符号,但你明白我的意思。)

所以这意味着 C/C++ 在引用数组变量时应该没有性能增益/损失。

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

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

那么判决结果是什么?我应该存储 int value = data[i] 并使用 value,还是没有性能影响?

【问题讨论】:

你为什么不试试看呢? 非常微不足道,我会假设,如果有的话。 在非常相似的情况下,我的团队注意到通过将索引值加载到局部变量并引用它而不是一直引用索引值可以显着节省。 @Jeremy Holovacs 当然。局部变量存储在堆栈中!如果它传播到 CPU 级别 3/2/1 缓存,它将比访问堆上的数组快很多倍。 【参考方案1】:

是的,由于每次访问数组的边界检查都会导致性能损失。

,您很可能不需要担心。

是的,您可以存储该值并使用该值。不,这不是因为性能问题,而是因为它使代码更具可读性(恕我直言)。


顺便说一句,JIT 编译器可能优化了冗余检查,因此这并不意味着您实际上会在每次调用时得到检查。无论哪种方式,可能都不值得您花时间担心它。只要使用它,如果它成为瓶颈,您可以随时返回并使用unsafe 块。

【讨论】:

因为实际上每个优秀的优化器(至少对于像 Java 或 C# 这样的语言来说,这不是普遍真理)都会进行通用子表达式消除,从而消除性能损失。并且在指定的代码中,无论如何都会使用边界检查。 我相信 JIT'ter 会优化边界检查以检查常见的循环,这样根本不会抛出超出范围的异常。【参考方案2】:

如果编译器可以证明数组没有被其他线程或循环内调用的任何方法(包括委托)访问,编译器只能在此处执行公共子表达式优化,最好自己创建本地副本。

但可读性应该是你主要关心的问题,除非这个循环执行很多次。

所有这些在 C 和 C++ 中也是如此——索引到数组将比访问局部变量慢。

附带说明,您建议的优化并不好:value 是关键字,请选择不同的变量名称。

【讨论】:

谢谢,是的,我的示例仅用于说明目的 - 为了方便起见,我通常会使用较短的名称,例如 'val' 或 'num'。【参考方案3】:

你已经用两种方式写了。双向运行,测量它。那你就知道了。

但我认为您更喜欢使用副本而不是总是直接使用数组元素,这仅仅是因为以这种方式编写代码更容易,特别是如果您有很多涉及该操作的操作特定的价值。

【讨论】:

【参考方案4】:

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

【讨论】:

据我记得,foreach 比 for 慢,并且有一个限制,即您无法修改正在处理的数组的大小。例如 foreach(object o in objectList)if(condition(o))objectList.Remove(o); 会抛出异常。【参考方案5】:

你可以吃蛋糕也可以吃。在很多情况下,抖动优化器可以很容易地确定数组索引访问是安全的并且不需要检查。像您在问题中遇到的任何 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 编译器生成的代码一样快。

【讨论】:

我认为解决 OP 的情况很重要。他提出来是对的,循环很重要。这就是低效代码的影响迅速成倍增加的地方。核心答案是它确实起作用,而不是“仅仅因为”或“可能优化”。 Hmya,抖动经过优化,可以在常见代码片段上产生良好的效果。它当然可以被打败。我不知道这种副手的好例子,当我得出结论“哇,这太棒了,我现在就用它”时,我停止了寻找。 @Hans:我有点,是的。 +1 现在实际上花时间拆卸。 :) 在这种情况下,您将真的欣赏 C# 进行数组绑定检查。抖动不会阻止你做错事,只有当你做对了才会有帮助。 数组元素类型无所谓。

以上是关于在 C# 中,引用数组变量会更慢吗?的主要内容,如果未能解决你的问题,请参考以下文章

当两个输入集之一是正常集时,zinterstore 会更快/更慢吗?

调整列表大小并在列表 C# 中引用变量位置

机械硬盘外接玩游戏加载更慢吗?

一个简单的多维数组解引用会很慢吗?

C#数据类型

基于 XML 字段创建视图