具有可空值类型的as运算符是否不必要地慢?

时间:2014-01-27 19:17:57

标签: c# performance nullable as-operator

考虑以下代码:

static void FillUsingAsNullable()
{
  int?[] arr = new int?[1 << 24];
  var sw = System.Diagnostics.Stopwatch.StartNew();
  for (int i = 0; i < arr.Length; ++i)
    arr[i] = GetObject() as int?;
  Console.WriteLine("{0:N0}", sw.ElapsedTicks);
}

static void FillUsingOwnCode()
{
  int?[] arr = new int?[1 << 24];
  var sw = System.Diagnostics.Stopwatch.StartNew();
  for (int i = 0; i < arr.Length; ++i)
  {
    object temporary = GetObject();
    arr[i] = temporary is int ? (int?)temporary : null;
  }
  Console.WriteLine("{0:N0}", sw.ElapsedTicks);
}

static object GetObject()
{
//Uncomment only one:
  //return new object();
  //return 42;
  //return null;
}

据我所知,方法FillUsingAsNullableFillUsingOwnCode应该是等效的。

但看起来“自有代码”版本显然更快。

2个选项可用于编译“x86”或“x64”,以及2选择用于编译“调试”或“发布(优化)”,以及3选择什么以GetObject方法返回。据我所知,在所有这些2*2*3 == 12个案例中,“自有代码”版本明显快于“可以为空”的版本。

问题:as Nullable<> {{1}}是否不必要地缓慢,或者我在这里遗漏了某些东西(很有可能)?

相关主题:Performance surprise with “as” and nullable types

3 个答案:

答案 0 :(得分:2)

生成的IL不同,但不是根本。如果JIT是好的,它不是,这不是新闻,这可以编译成完全相同的x86代码。

我用VS2010 Release AnyCPU编译了这个。

as版本:

L_0015: call object ConsoleApplication3.Program::GetObject()
L_001a: stloc.3 
L_001b: ldloc.0 
L_001c: ldloc.2 
L_001d: ldelema [mscorlib]System.Nullable`1<int32>
L_0022: ldloc.3 
L_0023: isinst [mscorlib]System.Nullable`1<int32>
L_0028: unbox.any [mscorlib]System.Nullable`1<int32>
L_002d: stobj [mscorlib]System.Nullable`1<int32>

?:版本:

L_0015: call object ConsoleApplication3.Program::GetObject()
L_001a: stloc.3 
L_001b: ldloc.0 
L_001c: ldloc.2 
L_001d: ldelema [mscorlib]System.Nullable`1<int32>
L_0022: ldloc.3 
L_0023: isinst int32
L_0028: brtrue.s L_0036 //**branch here**
L_002a: ldloca.s nullable
L_002c: initobj [mscorlib]System.Nullable`1<int32>
L_0032: ldloc.s nullable
L_0034: br.s L_003c
L_0036: ldloc.3 
L_0037: unbox.any [mscorlib]System.Nullable`1<int32>
L_003c: stobj [mscorlib]System.Nullable`1<int32>

操作码的描述在MSDN上。理解这个IL并不困难,任何人都可以做到。不过,这对于没有经验的人来说有点费时。

主要区别在于源代码中带分支的版本在生成的IL中也有一个分支。它只是不那么优雅。如果需要,C#编译器可以优化它,但团队的策略是让JIT担心优化。如果JIT获得必要的投资,那就行得很好。

您可以通过查看JIT发出的x86来进一步分析。你会发现一个明显的区别,但这将是一个不引人注目的发现。我不会花时间去做那件事。


我修改了as版本以使用临时版本进行公平比较:

            var temporary = GetObject();
            arr[i] = temporary as int?;

答案 1 :(得分:0)

是的, as 运算符是为了方便而不是性能而且速度较慢。

有关详细信息,请参阅以下答案:

How does the "as" keyword work internally?

答案 2 :(得分:0)

我认为您的测试结果受到测量误差的支配。

这是我的计划:

    static void Main()
    {
        FillUsingAsNullable();
        FillUsingOwnCode();
        FillUsingAsNullable();
        FillUsingOwnCode();
        Console.ReadLine();
    }

以下是我在Release,外部调试器中使用return 42运行的内容:

2,540,125
1,975,131
2,407,204
2,246,339

请注意,运行之间存在相当大的差异。您可能需要连续多次运行才能获得良好的性能指标。

以下是我们将GetObject()更改为(int?)42时的情况。

7,829,214
7,941,861
8,001,102
7,124,096

再次,使用相同的配置:

8,243,258
7,114,879
7,932,285
7,268,167

如果你真的想要收集有意义的数据,我建议用不同的顺序重复这两个测试,多次重复,然后查看结果均值和标准偏差。

我怀疑这些方法中最大的时间是内存缓存失效,因此给定测试的结果可能取决于为每个测试分配的数组到底的确切位置以及GC在频繁的装箱分配期间何时启动