340

我只是在深度修改处理可空类型的 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
作为: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);
    }
}
4

10 回答 10

212

显然,JIT 编译器可以为第一种情况生成的机器代码效率更高。一个真正有帮助的规则是,一个对象只能被拆箱为一个与装箱值具有相同类型的变量。这允许 JIT 编译器生成非常高效的代码,无需考虑值转换。

is运算符测试很简单,只需检查对象是否为 null 且是否为预期类型,只需要一些机器代码指令即可。转换也很容易,JIT 编译器知道对象中值位的位置并直接使用它们。不会发生复制或转换,所有机器代码都是内联的,只需要大约十几个指令。当拳击很常见时,这需要在 .NET 1.0 中非常有效。

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

Linq OfType() 扩展方法也使用is运算符和强制转换。然而,这是对泛型类型的强制转换。JIT 编译器生成对辅助函数 JIT_Unbox() 的调用,该函数可以执行强制转换为任意值类型。我没有很好的解释为什么它和 cast to 一样慢Nullable<int>,因为应该需要更少的工作。我怀疑 ngen.exe 可能会在这里造成麻烦。

于 2010-06-19T17:28:37.433 回答
26

在我看来,isinst可空类型真的很慢。在FindSumWithCast我改变的方法中

if (o is int)

if (o is int?)

这也大大减慢了执行速度。我能看到的 IL 的唯一区别是

isinst     [mscorlib]System.Int32

更改为

isinst     valuetype [mscorlib]System.Nullable`1<int32>
于 2009-10-17T20:10:02.613 回答
22

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

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

这是什么isinstECMA 335 Partition III, 4.6):

格式:isinst typeTok

typeTok是元数据标记(a或) typeref,表示所需的类。typedeftypespec

如果typeTok是不可为空的值类型或泛型参数类型,则将其解释为“装箱” typeTok

如果typeTok是一个可为空的类型,Nullable<T>它被解释为“装箱”T

最重要的是:

如果obj的实际类型(不是验证者跟踪的类型)是验证者可分配给类型 typeTok 的,那么isinst成功并且obj(作为result)原封不动地返回,而验证将其类型跟踪为typeTok与强制(§1.6)和转换(§3.27)不同,isinst从不改变对象的实际类型并保留对象身份(参见第一部分)。

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

为什么这样做?isinst T?从来没有明显的效果,即你得到一个T?. 相反,所有这些说明确保您拥有一个"boxed T"可以拆箱的T?. 为了得到一个实际的T?结果,我们仍然需要将我们的 to 拆箱"boxed T"T?这就是编译器发出unbox.anyafter的原因isinst。如果您考虑一下,这是有道理的,因为“盒子格式”T?只是一个"boxed T",而制作castclassisinst执行拆箱将是不一致的。

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

(ECMA 335 第三部分,4.33):unbox.any

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

(ECMA 335 第三部分,4.32):unbox

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

于 2011-08-15T09:50:41.873 回答
19

有趣的是,我通过dynamic慢一个数量级Nullable<T>(类似于这个早期测试)传递了关于操作员支持的反馈——我怀疑是出于非常相似的原因。

必须爱Nullable<T>。另一个有趣的事情是,即使 JIT 发现(并删除)null了不可为空的结构,它也会为以下内容而烦恼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;
    }
}
于 2009-10-17T21:26:50.370 回答
13

为了保持这个答案是最新的,值得一提的是,这个页面上的大部分讨论现在都没有实际意义,现在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
}

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

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

 


附录:这里给出了上面这个答案(分别)显示的前两个示例函数的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

// 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语法的性能超过以前可用选项的评论,请参见此处(特别是示例“D”)。

于 2017-08-21T01:30:56.043 回答
12

这是上面 FindSumWithAsAndHas 的结果:替代文字

这是 FindSumWithCast 的结果:替代文字

发现:

  • 使用as,它首先测试一个对象是否是 Int32 的实例;在它使用的引擎盖下isinst Int32(类似于手写代码: if (o is 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)

所以 cast ( (int)a[i],语法看起来像一个演员,但它实际上是拆箱,演员和拆箱共享相同的语法,下次我会用正确的术语来迂腐) 方法真的更快,你只需要拆箱一个值当一个对象绝对是一个int. 不能说使用as方法也是如此。

于 2010-06-19T14:04:30.403 回答
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);
    }

}

输出:

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 与 3524
  • 其次, .Value 比强制转换稍慢。3524 与 3272
  • 第三, .HasValue 比使用手动 has(即使用is)稍微慢一些。3524 与 3282
  • 第四,在模拟 asreal as方法之间进行苹果对苹果的比较(即同时分配模拟 HasValue 和转换模拟 Value),我们可以看到模拟 as仍然比真实 as快得多。395 与 3524
  • 最后,基于第一个和第四个结论,作为 实现有问题^_^
于 2010-06-21T14:17:32.887 回答
8

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

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

作为

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

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

于 2009-10-17T19:59:51.323 回答
8

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

typeof(int) == item.GetType(),它的执行速度与版本一样快item is int,并且始终返回数字(强调:即使您将 aNullable<int>写入数组,您也需要使用typeof(int))。您还需要在此处进行额外null != item检查。

然而

typeof(int?) == item.GetType()保持快速(与 相比item is int?),但总是返回 false。

typeof-construct 在我看来是精确类型检查的最快方法,因为它使用 RuntimeTypeHandle。由于这种情况下的确切类型与 nullable 不匹配,我的猜测是,is/as必须在此处进行额外的繁重工作,以确保它实际上是 Nullable 类型的实例。

老实说:你给你is Nullable<xxx> plus HasValue买了什么?没有什么。您始终可以直接转到基础(值)类型(在这种情况下)。您要么获得该值,要么获得“不,不是您要求的类型的实例”。即使您写入(int?)null数组,类型检查也会返回 false。

于 2010-06-19T10:01:21.650 回答
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);
    }
}

输出:

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

[编辑:2010-06-19]

注意:之前的测试是在VS内部进行的,配置调试,使用VS2009,使用Core i7(公司开发机)。

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

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
于 2010-04-21T00:28:15.413 回答