猿问
下载APP

“as”和可空类型的性能惊喜

“as”和可空类型的性能惊喜

我刚刚修改了深度C#的第4章,它处理了可空类型,我正在添加一个关于使用“as”运算符的部分,它允许你编写:

object o = ...;int? x = o as int?;if (x.HasValue){
    ... // Use x.Value in here}

我认为这非常简洁,它可以提高性能而不是C#1等效,使用“is”后跟一个演员 - 毕竟,这样我们只需要请求动态类型检查一次,然后进行简单的值检查。

然而,情况似乎并非如此。我在下面包含了一个示例测试应用程序,它基本上对对象数组中的所有整数求和 - 但该数组包含许多空引用和字符串引用以及盒装整数。该基准测试您必须在C#1中使用的代码,使用“as”运算符的代码,以及用于踢LINQ解决方案的代码。令我惊讶的是,在这种情况下,C#1代码的速度提高了20倍 - 即使是LINQ代码(考虑到所涉及的迭代器,我预计它会更慢)也胜过“as”代码。

可以isinst为空的类型的.NET实现真的很慢吗?是unbox.any导致问题的附加因素吗?还有另一种解释吗?目前,我觉得我必须在性能敏感的情况下包含警告,禁止使用它...

结果:

演员:10000000:121 
As:10000000:2211 
LINQ:10000000:2143

码:

using System;using System.Diagnostics;using System.Linq;class Test{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i+1] = "";
            values[i+2] = 1;
        }

        FindSumWithCast(values);
        FindSumWithAs(values);
        FindSumWithLinq(values);
    }

    static void FindSumWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int) o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }

    static void FindSumWithAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }
暮色呼如
浏览 42回答 3
3回答

富国沪深

显然,JIT编译器可以为第一种情况生成的机器代码更加高效。一个真正有用的规则是,只能将对象取消装箱到与盒装值具有相同类型的变量。这允许JIT编译器生成非常有效的代码,不必考虑任何值转换。的就是运营商的测试很容易,只要检查对象是不是null,而是预期的类型的,但需要一些机器代码指令。转换也很简单,JIT编译器知道对象中值位的位置并直接使用它们。没有复制或转换,所有机器代码都是内联的,只需要十几条指令。当拳击很常见时,这需要在.NET 1.0中真正有效。转为int?需要做更多的工作。盒装整数的值表示与内存布局不兼容Nullable<int>。由于可能的盒装枚举类型,需要进行转换并且代码很棘手。JIT编译器生成对名为JIT_Unbox_Nullable的CLR帮助函数的调用,以完成工作。这是任何值类型的通用函数,有很多代码用于检查类型。并且值被复制。由于此代码被锁定在mscorwks.dll中,因此很难估算成本,但很可能有数百条机器代码指令。Linq OfType()扩展方法也使用了is运算符和强制转换。然而,这是对通用类型的强制转换。JIT编译器生成对辅助函数JIT_Unbox()的调用,该函数可以执行强制转换为任意值类型。我没有一个很好的解释,为什么它与演员一样慢Nullable<int>,因为应该做的工作少。我怀疑ngen.exe可能会在这里造成麻烦。

若吾皇

这是上面的FindSumWithAsAndHas的结果:&nbsp;这是FindSumWithCast的结果:&nbsp;发现:使用时as,首先测试一个对象是否是Int32的一个实例;&nbsp;它正在使用isinst Int32(它类似于手写代码:if(o是int))。并且使用时as,它也无条件地拆除对象。而且它是一个真正的性能杀手来称呼一个属性(它仍然是引擎盖下的一个功能),IL_0027使用强制转换,首先测试对象是否为int&nbsp;if (o is int);&nbsp;在引擎盖下这是使用isinst Int32。如果它是int的实例,那么您可以安全地取消装入值IL_002D简单地说,这是使用as方法的伪代码:int?&nbsp;x;(x.HasValue,&nbsp;x.Value)&nbsp;=&nbsp;(o&nbsp;isinst&nbsp;Int32,&nbsp;o&nbsp;unbox&nbsp;Int32)if&nbsp;(x.HasValue) &nbsp;&nbsp;&nbsp;&nbsp;sum&nbsp;+=&nbsp;x.Value;这是使用强制转换方法的伪代码:if&nbsp;(o&nbsp;isinst&nbsp;Int32) &nbsp;&nbsp;&nbsp;&nbsp;sum&nbsp;+=&nbsp;(o&nbsp;unbox&nbsp;Int32)所以演员表((int)a[i]好吧,语法看起来像一个演员,但实际上是拆箱,演员和拆箱共享相同的语法,下次我会用正确的术语迂腐)方法真的更快,你只需要取消装箱值当一个物体明确地是一个int。使用as方法不能说同样的事情。

沧海一幻觉

为了使这个答案保持最新,值得一提的是,现在使用C#7.1和.NET 4.7来讨论此页面上的大部分讨论都是没有用的,它支持一种纤薄的语法,它也能产生最好的IL代码。OP的原始例子......object o = ...;int? x = o as int?;if (x.HasValue){&nbsp; &nbsp; // ...use x.Value in here}变得简单......if (o is int x){&nbsp; &nbsp; // ...use x in here}我发现新语法的一个常见用途是当你编写一个.NET 值类型(即struct在C#中)实现IEquatable<MyStruct>(大多数应该)。在实现强类型Equals(MyStruct other)方法之后,您现在可以优雅地将无类型Equals(Object obj)重写(继承自Object)重定向到它,如下所示:public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);&nbsp;附录:这里给出了本答案中分别显示的前两个示例函数的Release构建IL代码。虽然新语法的IL代码确实小了1个字节,但它通常通过进行零调用(相对于两个)并unbox在可能的情况下完全避免操作来获胜。// static void test1(Object o, ref int y)// {//&nbsp; &nbsp; &nbsp;int? x = o as int?;//&nbsp; &nbsp; &nbsp;if (x.HasValue)//&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;y = x.Value;// }[0] valuetype [mscorlib]Nullable`1<int32> x&nbsp; &nbsp; &nbsp; &nbsp; ldarg.0&nbsp; &nbsp; &nbsp; &nbsp; isinst [mscorlib]Nullable`1<int32>&nbsp; &nbsp; &nbsp; &nbsp; unbox.any [mscorlib]Nullable`1<int32>&nbsp; &nbsp; &nbsp; &nbsp; stloc.0&nbsp; &nbsp; &nbsp; &nbsp; ldloca.s x&nbsp; &nbsp; &nbsp; &nbsp; call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()&nbsp; &nbsp; &nbsp; &nbsp; brfalse.s L_001e&nbsp; &nbsp; &nbsp; &nbsp; ldarg.1&nbsp; &nbsp; &nbsp; &nbsp; ldloca.s x&nbsp; &nbsp; &nbsp; &nbsp; call instance !0 [mscorlib]Nullable`1<int32>::get_Value()&nbsp; &nbsp; &nbsp; &nbsp; stind.i4L_001e: ret// static void test2(Object o, ref int y)// {//&nbsp; &nbsp; &nbsp;if (o is int x)//&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;y = x;// }[0] int32 x,[1] object obj2&nbsp; &nbsp; &nbsp; &nbsp; ldarg.0&nbsp; &nbsp; &nbsp; &nbsp; stloc.1&nbsp; &nbsp; &nbsp; &nbsp; ldloc.1&nbsp; &nbsp; &nbsp; &nbsp; isinst int32&nbsp; &nbsp; &nbsp; &nbsp; ldnull&nbsp; &nbsp; &nbsp; &nbsp; cgt.un&nbsp; &nbsp; &nbsp; &nbsp; dup&nbsp; &nbsp; &nbsp; &nbsp; brtrue.s L_0011&nbsp; &nbsp; &nbsp; &nbsp; ldc.i4.0&nbsp; &nbsp; &nbsp; &nbsp; br.s L_0017L_0011: ldloc.1&nbsp; &nbsp; &nbsp; &nbsp; unbox.any int32L_0017: stloc.0&nbsp; &nbsp; &nbsp; &nbsp; brfalse.s L_001d&nbsp; &nbsp; &nbsp; &nbsp; ldarg.1&nbsp; &nbsp; &nbsp; &nbsp; ldloc.0&nbsp; &nbsp; &nbsp; &nbsp; stind.i4L_001d: ret有关进一步测试证实了我对新C#7语法的性能超过以前可用选项的评论,请参见此处(特别是示例“D”)。
打开App,查看更多内容
随时随地看视频慕课网APP
我要回答