【问题标题】:C# performance using array index [duplicate]使用数组索引的 C# 性能
【发布时间】:2014-01-13 19:57:16
【问题描述】:

我有一个数组,我想在同一个方法中多次使用单个值。

int[] array = new array[] {1, 2, 3};

现在我问你如果我使用数组和索引来获取值是否是一个大的性能问题...

int x = array[1] * 2;
int y = array[1] * 3;
int z = array[1] * 4;

还是创建一个局部变量更好?

int value = array[1];
int x = value * 2;
int y = value * 3;
int z = value * 4;

我知道使用局部变量更容易阅读,但它只是让我感兴趣,如果它有任何性能差异。 ;-)

【问题讨论】:

  • 我会选择第二种方法,不仅是为了提高性能,也是为了避免重复。
  • 微优化是邪恶的
  • 即使您确实想优化它,为什么不编写一个小程序来测试它经过多次迭代(就像 Konrad 在他的回答中所做的那样)?
  • @Steve 请记住,基准测试很难,对于微优化来说更是如此。 超级很容易得到非常误导性的结果,最终只表明基准代码的性能差异,而不是正在测试的代码。

标签: c# arrays performance


【解决方案1】:

虽然我同意这样的微优化是邪恶的和不必要的,并且可读性可能更重要,但为两种方法制作虚拟基准还是很有趣的:

private static int TestWithIndex(int[] array)
{
    int x = array[1] * 2;
    int y = array[1] * 3;
    int z = array[1] * 4;
    return x + y + z;
}

private static int TestWithTemp(int[] array)
{
    int value = array[1];
    int x = value * 2;
    int y = value * 3;
    int z = value * 4;
    return x + y + z;
}

在Release 模式下调用它们int.MaxValue 次会产生:

  • 12032 毫秒 - 对于TestWithIndex
  • 10525 毫秒 - 对于TestWithTemp

然后我们看一下生成的IL(发布模式,启用优化):

TestWithIndex

.method private hidebysig static 
    int32 TestWithIndex (
        int32[] 'array'
    ) cil managed 
{
    // Method begins at RVA 0x2564
    // Code size 29 (0x1d)
    .maxstack 2
    .locals init (
        [0] int32 x,
        [1] int32 y,
        [2] int32 z,
        [3] int32 CS$1$0000
    )

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldc.i4.1
    IL_0003: ldelem.i4
    IL_0004: ldc.i4.2
    IL_0005: mul
    IL_0006: stloc.0
    IL_0007: ldarg.0
    IL_0008: ldc.i4.1
    IL_0009: ldelem.i4
    IL_000a: ldc.i4.3
    IL_000b: mul
    IL_000c: stloc.1
    IL_000d: ldarg.0
    IL_000e: ldc.i4.1
    IL_000f: ldelem.i4
    IL_0010: ldc.i4.4
    IL_0011: mul
    IL_0012: stloc.2
    IL_0013: ldloc.0
    IL_0014: ldloc.1
    IL_0015: add
    IL_0016: ldloc.2
    IL_0017: add
    IL_0018: stloc.3
    IL_0019: br.s IL_001b

    IL_001b: ldloc.3
    IL_001c: ret
} // end of method Program::TestWithIndex

这里我们看到三个ldelem.i4。

TestWithTemp

.method private hidebysig static 
    int32 TestWithTemp (
        int32[] 'array'
    ) cil managed 
{
    // Method begins at RVA 0x2590
    // Code size 29 (0x1d)
    .maxstack 2
    .locals init (
        [0] int32 'value',
        [1] int32 x,
        [2] int32 y,
        [3] int32 z,
        [4] int32 CS$1$0000
    )

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldc.i4.1
    IL_0003: ldelem.i4
    IL_0004: stloc.0
    IL_0005: ldloc.0
    IL_0006: ldc.i4.2
    IL_0007: mul
    IL_0008: stloc.1
    IL_0009: ldloc.0
    IL_000a: ldc.i4.3
    IL_000b: mul
    IL_000c: stloc.2
    IL_000d: ldloc.0
    IL_000e: ldc.i4.4
    IL_000f: mul
    IL_0010: stloc.3
    IL_0011: ldloc.1
    IL_0012: ldloc.2
    IL_0013: add
    IL_0014: ldloc.3
    IL_0015: add
    IL_0016: stloc.s CS$1$0000
    IL_0018: br.s IL_001a

    IL_001a: ldloc.s CS$1$0000
    IL_001c: ret
} // end of method Program::TestWithTemp

这里当然只有一个ldelem.i4。

【讨论】:

  • 感谢您的快速答复!我以为它不会有很大的影响,但它只是让我感兴趣。 :-)
【解决方案2】:

不,不会有性能差异。为此:

int x = array[1] * 2;

array[1] 处的值无论如何都必须在生成 IL 时移动到内存位置。然后编译器将优化剩余的操作(即它不会多次检索该值)。


好吧,为了解决争论,我决定抛弃每个,这是第一个:

.method private hidebysig static void  Main(string[] args) cil managed
{
  IL_0000:  nop
  IL_0001:  ldc.i4.3
  IL_0002:  newarr     [mscorlib]System.Int32
  IL_0007:  dup
  IL_0008:  ldtoken    field valuetype '<PrivateImplementationDetails>{79A4FD92-FA37-4EB9-8056-B52A57262FBB}'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>{79A4FD92-FA37-4EB9-8056-B52A57262FBB}'::'$$method0x6000001-1'
  IL_000d:  call       void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array,
                                                                                                      valuetype [mscorlib]System.RuntimeFieldHandle)
  IL_0012:  stloc.0
  IL_0013:  ldloc.0
  IL_0014:  ldc.i4.1
  IL_0015:  ldelem.i4
  IL_0016:  stloc.1
  IL_0017:  ldloc.1
  IL_0018:  ldc.i4.2
  IL_0019:  mul
  IL_001a:  stloc.2
  IL_001b:  ldloc.1
  IL_001c:  ldc.i4.3
  IL_001d:  mul
  IL_001e:  stloc.3
  IL_001f:  ldloc.1
  IL_0020:  ldc.i4.4
  IL_0021:  mul
  IL_0022:  stloc.s    z
  IL_0024:  ret
} // end of method Program::Main

这是第二个:

.method private hidebysig static void  Main(string[] args) cil managed
{
  IL_0000:  nop
  IL_0001:  ldc.i4.3
  IL_0002:  newarr     [mscorlib]System.Int32
  IL_0007:  dup
  IL_0008:  ldtoken    field valuetype '<PrivateImplementationDetails>{79A4FD92-FA37-4EB9-8056-B52A57262FBB}'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>{79A4FD92-FA37-4EB9-8056-B52A57262FBB}'::'$$method0x6000001-1'
  IL_000d:  call       void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array,
                                                                                                      valuetype [mscorlib]System.RuntimeFieldHandle)
  IL_0012:  stloc.0
  IL_0013:  ldloc.0
  IL_0014:  ldc.i4.1
  IL_0015:  ldelem.i4
  IL_0016:  stloc.1
  IL_0017:  ldloc.1
  IL_0018:  ldc.i4.2
  IL_0019:  mul
  IL_001a:  stloc.2
  IL_001b:  ldloc.1
  IL_001c:  ldc.i4.3
  IL_001d:  mul
  IL_001e:  stloc.3
  IL_001f:  ldloc.1
  IL_0020:  ldc.i4.4
  IL_0021:  mul
  IL_0022:  stloc.s    z
  IL_0024:  ret
} // end of method Program::Main

它们完全一样——正如我所说的那样。

【讨论】:

  • (i.e. it's not going to retrieve the value more than once) 你怎么知道的?它不知道数组值在执行时不会改变,所以它需要再次访问它。
  • 另一个线程可能正在更改它们。这也意味着您假设编译器正在执行分析以尝试确定是否在访问之间设置了该特定值。这是一件非常不平凡的事情。如果在当前范围之外调用另一个方法怎么办,如果触发了可能具有改变数组的处理程序的事件怎么办等。实际检查的可能性太多。
  • @user2864740 所以你认为编译器会检查是否存在某种形式的同步/内存屏障,如果存在则只执行查找?请记住,它可能不在当前范围内,但可能隐藏在另一个方法中,等等。您还可以使用我讨论过的其他相同线程选项;来自被调用方法的突变、触发的事件、调用的其他类型的委托,可能通过任意数量的间接层(即使用延迟执行来改变数组)等等。要检查的事情太多了。
  • @user2864740 这对于运行时来说仍然很难做到;知道内存中的哪些位置可以放心地访问而无需更改。内存最终已经被缓存缓存了,缓存的唯一目的是非常非常快地重复访问内存,同时仍然保持数据完整性。没有理由这样做,即使可以。
  • TestWithIndex 和 TestWithTemp 在我的示例中不是同一个 IL。
【解决方案3】:

如果您没有性能要求或性能问题,那么您的主要目标是编写易于维护的可读代码。在您的第一个示例中很容易看到重复:

int x = array[1] * 2;
int y = array[1] * 3;
int z = array[1] * 4;

它有几个问题。首先 - 您复制的代码越多,您必须支持的代码就越多,并且您在某个时间点不会修改其中一个代码副本的可能性就越大。其次 - 重复总是意味着您在代码中隐藏了知识。如果某些代码被重复,那么它具有特定的含义,您没有明确表示。例如。您在数组的第二项中有速度值。明确说明这些知识:

int speed = array[1];
int x = speed * 2; // of course, magic numbers also should be replaced
int y = speed * 3;
int z = speed * 4;

请记住 - 过早的优化是邪恶的。通常你有 20% 的代码需要 80% 的执行时间。您的优化很可能不会影响应用程序的性能。所以,你应该首先找到这 20%,然后才进行优化(如果他们真的需要的话)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-07-01
    • 2012-11-10
    • 2011-03-26
    • 2023-03-04
    • 2023-03-06
    • 2016-03-19
    • 2010-10-28
    相关资源
    最近更新 更多