首页 文章

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

提问于
浏览
307

我只修改了深度中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”代码 .

可空类型的.NET实现 isinst 真的很慢吗?这是导致问题的额外 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);
    }
}

10 回答

  • 12

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

    if (o is int)
    

    if (o is int?)
    

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

    isinst     [mscorlib]System.Int32
    

    变了

    isinst     valuetype [mscorlib]System.Nullable`1<int32>
    
  • 7

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

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

    这是 isinst 的作用(ECMA 335 Partition III, 4.6):

    格式:isinst typeTok typeTok是元数据标记(typeref,typedef或typespec),表示所需的类 . 如果typeTok是非可空值类型或通用参数类型,则将其解释为“boxed”typeTok . 如果typeTok是可空类型,Nullable <T>,则将其解释为“boxed”T

    最重要的是:

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

    因此,在这种情况下,性能杀手不是 isinst ,而是额外的 unbox.any . 这不是't clear from Hans'答案,因为他只看了JITed代码 . 通常,C#编译器将在 isinst T? 之后发出 unbox.any (但如果 isinst TT 是引用类型,则会省略它) .

    为什么这样做? isinst T? 永远不会有明显的效果,即你回来了 T? . 相反,所有这些说明确保您有 "boxed T" 可以取消装箱 T? . 要获得实际的 T? ,我们仍然需要将 "boxed T" 解包为 T? ,这就是编译器在 isinst 之后发出 unbox.any 的原因 . 如果你考虑一下,这是有道理的,因为"box format"对于 T? 只是一个 "boxed T" 并且使 castclassisinst 执行unbox会不一致 .

    通过standard中的一些信息来支持汉斯的发现,这里有:

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

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

    (ECMA 335 Partition III,4.32): unbox

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

  • 9

    显然,JIT编译器可以为第一种情况生成的机器代码更加高效 . 一个真正有用的规则是,对象只能拆箱到与盒装值具有相同类型的变量 . 这允许JIT编译器生成非常有效的代码,不必考虑任何值转换 .

    运算符测试很简单,只需检查对象是否为空且是否为预期类型,只需要几个机器代码指令 . 转换也很简单,JIT编译器知道对象中值位的位置并直接使用它们 . 没有复制或转换,所有机器代码都是内联的,只需要十几条指令 . 当拳击很常见时,这需要在.NET 1.0中真正有效 .

    投射到int?需要做更多的工作 . 盒装整数的值表示与 Nullable<int> 的内存布局不兼容 . 由于可能的盒装枚举类型,因此需要进行转换并且代码很棘手 . JIT编译器生成对名为JIT_Unbox_Nullable的CLR帮助函数的调用,以完成工作 . 这是任何值类型的通用函数,有很多代码用于检查类型 . 并且值被复制 . 很难估计成本,因为此代码被锁定在mscorwks.dll中,但很可能有数百条机器代码指令 .

    Linq OfType()扩展方法也使用了is运算符和强制转换 . 然而,这是对通用类型的强制转换 . JIT编译器生成对辅助函数JIT_Unbox()的调用,该函数可以执行强制转换为任意值类型 . 我没有一个很好的解释,为什么它和演员一样慢,因为应该做的工作少 . 我怀疑ngen.exe可能会在这里引起麻烦 .

  • 22

    有趣的是,我通过 dynamic 传递了关于操作员支持的反馈,对于 Nullable<T> (类似于this early test)来说是一个数量级的慢 - 我怀疑是非常相似的原因 .

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

    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;
        }
    }
    
  • 7

    这是上面的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;
    

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

    if (o isinst Int32)
        sum += (o unbox Int32)
    

    因此,演员( (int)a[i] ,语法看起来像一个演员,但是它具有正确的术语迂腐)方法真的更快,你只需要在一个对象肯定是 int 时取消装箱 . 同样的事情不能说使用 as 方法 .

  • 197

    进一步剖析:

    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);
        }
    
    }
    

    输出:

    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
    

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

    • 首先,is-then-cast方法明显快于 as 方法 . 303 vs 3524

    • 其次,.Value比铸造慢一点 . 3524对3272

    • 第三,.HasValue略微慢于使用手动(即使用 is ) . 3524对3282

    • 第四,在 simulated asreal as 方法之间做一个苹果到苹果的比较(即分配模拟的HasValue和转换模拟值一起发生),我们可以看到 simulated as 仍然明显快于 real as . 395对3524

    • 最后,根据第一和第四个结论, as 实现^ _ ^有问题

  • 19

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

    foreach (object o in values)
            {
                int? x = o as int?;
    

    int? x;
    foreach (object o in values)
            {
                x = o as int?;
    

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

  • 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 给你带来了什么?没有 . 您始终可以直接转到底层(值)类型(在本例中) . 你要么得到 Value ,要么"no, not an instance of the type you were asking for" . 即使您将 (int?)null 写入数组,类型检查也将返回false .

  • 23
    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);
        }
    }
    

    输出:

    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
    

    [EDIT: 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
    
  • 8

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

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

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

    变得简单......

    if (o is int x)
    {
        // ...use x in here
    }
    

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

    public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
    

    Appendix: 此处给出了本答案中分别显示的前两个示例函数的 Release build 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
    
    // 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
    

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

相关问题