"as"和可空类型的性能惊喜

Jon*_*eet 326 c# clr performance unboxing nullable

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

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

我认为这非常简洁,它可以提高性能而不是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);
    }

    static void FindSumWithLinq(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = values.OfType<int>().Sum();
        sw.Stop();
        Console.WriteLine("LINQ: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }
}
Run Code Online (Sandbox Code Playgroud)

Han*_*ant 206

显然,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可能会在这里造成麻烦.

  • 好的,我确信.我想我习惯认为"是"因为继承层次结构的可能性而可能很昂贵 - 但在值类型的情况下,不存在层次结构的可能性,因此它可以是一个简单的按位比较.我仍然认为可以通过JIT来优化可空案例的JIT代码. (16认同)

Dir*_*mar 26

在我看来,isinst在可空类型上真的很慢.方法FindSumWithCast我改变了

if (o is int)
Run Code Online (Sandbox Code Playgroud)

if (o is int?)
Run Code Online (Sandbox Code Playgroud)

这也显着减慢了执行速度.我可以看到IL的唯一不同之处在于

isinst     [mscorlib]System.Int32
Run Code Online (Sandbox Code Playgroud)

变了

isinst     valuetype [mscorlib]System.Nullable`1<int32>
Run Code Online (Sandbox Code Playgroud)

  • 不仅如此;在“强制转换”的情况下,`isinst` 之后是无效性测试,然后*有条件地* 是`unbox.any`。在可为空的情况下,有一个*无条件*的`unbox.any`。 (2认同)

Joh*_*lph 22

这最初是作为对Hans Passant的优秀答案的评论开始的,但它太长了所以我想在这里添加一些内容:

首先,C#as运算符将发出isinstIL指令(is运算符也是如此).(另一个有趣的指令是castclass,当你进行直接转换时发出,编译器知道不能省略运行时检查.)

这是做什么的isinst(ECMA 335 Partition III,4.6):

格式:isinst typeTok

typeTok是元数据令牌(一个typeref,typedeftypespec),这表明所期望的类.

如果typeTok是非可空值类型或通用参数类型,则将其解释为"boxed" typeTok.

如果typeTok是可空类型,Nullable<T>则将其解释为"盒装"T

最重要的是:

如果obj的实际类型(不是验证者跟踪类型)是verifier-assignable-类型typeTok则isinst成功并且obj(作为结果)不变地返回,而验证将其类型跟踪为typeTok.与强制(§1.6)和转换(§3.27)不同,isinst永远不会更改对象的实际类型并保留对象标识(请参阅分区I).

因此,性能杀手不是isinst在这种情况下,而是额外的unbox.any.汉斯的回答并不清楚,因为他只看了JITed代码.通常,C#编译器将unbox.any在a之后发出isinst T?(但是如果你这样做将省略它isinst T,何时T是引用类型).

为什么这样做?isinst T?永远不会有明显的效果,即你回来了T?.相反,所有这些说明都确保您拥有"boxed T"可以取消装箱的指令T?.为了获得一个实际的T?,我们仍然需要拆箱我们"boxed T"T?,这就是为什么编译器的发射unbox.any之后isinst.如果你考虑一下,这是有道理的,因为"盒子格式" T?只是一个,"boxed T"并且制作castclassisinst执行unbox会不一致.

标准中的一些信息来支持汉斯的发现,这里是:

(ECMA 335 Partition III,4.33): unbox.any

当应用于值类型的盒装形式时,该unbox.any指令将提取obj(类型O)中包含的值.(相当于unbox后面跟着ldobj.)当应用于引用类型时,该unbox.any指令与castclasstypeTok 具有相同的效果.

(ECMA 335 Partition III,4.32): unbox

通常,unbox只需计算已装箱对象内部已存在的值类型的地址.取消装箱可以为空的值类型时,这种方法是不可行的.由于Nullable<T>Ts在框操作期间转换为盒装,因此实现通常必须Nullable<T>在堆上制造新的并计算新分配的对象的地址.


Mar*_*ell 19

有趣的是,我传递了关于操作员支持的反馈,因为dynamic它的速度比较慢Nullable<T>(类似于这个早期测试) - 我怀疑是出于非常相似的原因.

得爱Nullable<T>.另一个有趣的是,即使JIT发现(并删除)null不可为空的结构,它也会将其borks Nullable<T>:

using System;
using System.Diagnostics;
static class Program {
    static void Main() { 
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Justin - 泛型方法可以在运行时使用任意数量的泛型参数排列(`T`等).堆栈等要求取决于args(本地的堆栈空间量等),因此您可以获得一个涉及值类型的任何唯一排列的JIT.但是,引用的大小都相同,因此共享一个JIT.在执行per-value-type JIT时,它可以检查一些明显的场景,并且*尝试*来删除由于诸如不可能的null之类的事情而无法访问的代码.注意,它并不完美.另外,我忽略了上述的AOT. (2认同)

Mic*_*uen 12

这是上面FindSumWithAsAndHas的结果:alt text http://www.freeimagehosting.net/uploads/9e3c0bfb75.png

这是FindSumWithCast的结果:alt text http://www.freeimagehosting.net/uploads/ce8a5a3934.png

发现:

  • 使用时as,首先测试一个对象是否是Int32的一个实例; 它正在使用isinst Int32(它类似于手写代码:if(o是int)).并且使用时as,它也无条件地拆除对象.而且它是一个真正的性能杀手来称呼一个属性(它仍然是引擎盖下的一个功能),IL_0027

  • 使用强制转换,首先测试对象是否为int if (o is int); 在引擎盖下这是使用isinst Int32.如果它是int的实例,那么您可以安全地取消装入值IL_002D

简单地说,这是使用as方法的伪代码:

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    
Run Code Online (Sandbox Code Playgroud)

这是使用强制转换方法的伪代码:

if (o isinst Int32)
    sum += (o unbox Int32)
Run Code Online (Sandbox Code Playgroud)

所以演员表((int)a[i]好吧,语法看起来像一个演员,但它实际上是拆箱,演员和拆箱共享相同的语法,下次我会用正确的术语迂腐)方法真的更快,你只需要取消装箱值当一个物体明确地是一个int.使用as方法不能说同样的事情.


Mic*_*uen 9

进一步剖析:

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

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

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}
Run Code Online (Sandbox Code Playgroud)

输出:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282
Run Code Online (Sandbox Code Playgroud)

我们可以从这些数字中推断出什么?

  • 首先,-然后浇铸方法比显著更快方法.303 vs 3524
  • 其次,.Value比铸造略慢.3524对3272
  • 第三,.HasValue略微慢于使用手动(即使用is).3524对3282
  • 第四,在模拟的真实的方法之间做一个苹果到苹果的比较(即分配模拟的HasValue和转换模拟的值一起发生),我们可以看到模拟的仍然明显快于真实的.395对3524
  • 最后,基于第一和第四个结论,有一些错误 实现^ _ ^


Gle*_*den 9

为了使这个答案保持最新,值得一提的是,现在使用C#7.1.NET 4.7来讨论此页面上的大部分讨论都是没有用的,它支持一种纤薄的语法,它也能产生最好的IL代码.

OP的原始例子......

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}
Run Code Online (Sandbox Code Playgroud)

变得简单......

if (o is int x)
{
    // ...use x in here
}
Run Code Online (Sandbox Code Playgroud)

我发现新语法的一个常见用途是当你编写一个.NET 值类型(即structC#中)实现IEquatable<MyStruct>(大多数应该).在实现强类型Equals(MyStruct other)方法之后,您现在可以优雅地将无类型Equals(Object obj)重写(继承自Object)重定向到它,如下所示:

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
Run Code Online (Sandbox Code Playgroud)

 


附录:这里给出了本答案中分别显示的前两个示例函数的Release构建IL代码.虽然新语法的IL代码确实小了1个字节,但它通常通过进行零调用(相对于两个)并unbox在可能的情况下完全避免操作来获胜.

// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret
Run Code Online (Sandbox Code Playgroud)

// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret
Run Code Online (Sandbox Code Playgroud)

有关进一步测试证实了我对新C#7语法的性能超过以前可用选项的评论,请参见此处(特别是示例"D").


Jam*_*ack 8

我没时间尝试,但你可能想要:

foreach (object o in values)
        {
            int? x = o as int?;
Run Code Online (Sandbox Code Playgroud)

int? x;
foreach (object o in values)
        {
            x = o as int?;
Run Code Online (Sandbox Code Playgroud)

您每次都在创建一个新对象,这不会完全解释问题,但可能会有所帮助.

  • 当创建方法的堆栈帧时,在堆栈上分配局部变量,因此在方法中声明变量根本没有任何区别.(当然,除非它处于关闭状态,但这不是这种情况.) (4认同)
  • 在我的经验中,在其他位置声明变量只会在捕获变量时才显着影响生成的代码(此时会影响实际的语义)。注意,虽然它肯定是在使用unbox.any在堆栈上创建新的int实例,但并未在堆上创建新对象。我怀疑这就是问题所在-我的猜测是,手工制作的IL在这里可能会胜过这两种选择……尽管JIT也有可能经过优化以识别is / cast情况,并且只检查一次。 (2认同)

dal*_*alo 8

我尝试了确切的类型检查构造

typeof(int) == item.GetType(),执行速度和item is int版本一样快,并且总是返回数字(强调:即使你写了一个Nullable<int>数组,你也需要使用typeof(int)).您还需要null != item在此处进行额外检查.

然而

typeof(int?) == item.GetType()保持快速(与之相反item is int?),但始终返回false.

在我看来,typeof-construct是精确类型检查的最快方式,因为它使用RuntimeTypeHandle.由于这种情况下的确切类型与可空的不匹配,我的猜测是,is/as必须在此确保它实际上是Nullable类型的实例.

老实说:你is Nullable<xxx> plus HasValue买什么?没有.您始终可以直接转到基础(值)类型(在本例中).您要么获得值,要么"不,不是您要求的类型的实例".即使您写入(int?)null数组,类型检查也将返回false.


Mic*_*uen 7

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);
        FindSumWithAsAndHas(values);
        FindSumWithAsAndIs(values);


        FindSumWithIsThenAs(values);
        FindSumWithIsThenConvert(values);

        FindSumWithLinq(values);



        Console.ReadLine();
    }

    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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }


    static void FindSumWithAsAndIs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Is: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }







    static void FindSumWithIsThenAs(object[] values)
    {
        // Apple-to-apple comparison with Cast routine above.
        // Using the similar steps in Cast routine above,
        // the AS here cannot be slower than Linq.



        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {

            if (o is int)
            {
                int? x = o as int?;
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then As: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

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



    static void FindSumWithLinq(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = values.OfType<int>().Sum();
        sw.Stop();
        Console.WriteLine("LINQ: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811
Run Code Online (Sandbox Code Playgroud)

[编辑:2010-06-19]

注意:先前的测试是在VS,配置调试中使用VS2009,使用Core i7(公司开发机器)完成的.

使用VS2010,使用Core 2 Duo在我的机器上完成以下操作

Inside VS, Configuration: Debug

Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018




Outside VS, Configuration: Debug

Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944




Inside VS, Configuration: Release

Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932




Outside VS, Configuration: Release

Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936
Run Code Online (Sandbox Code Playgroud)