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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1583050/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 19:02:46  来源:igfitidea点击:

Performance surprise with "as" and nullable types

c#performanceclrnullableunboxing

提问by Jon Skeet

I'm just revising chapter 4 of C# in Depth which deals with nullable types, and I'm adding a section about using the "as" operator, which allows you to write:

我只是修改了 C# in Depth 中处理可为空类型的第 4 章,并且我正在添加一个关于使用“as”运算符的部分,它允许您编写:

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

I thought this was really neat, and that it could improve performance over the C# 1 equivalent, using "is" followed by a cast - after all, this way we only need to ask for dynamic type checking once, and then a simple value check.

我认为这真的很巧妙,它可以提高 C# 1 等效项的性能,使用“is”后跟强制转换 - 毕竟,这样我们只需要请求一次动态类型检查,然后进行简单的值检查.

This appears not to be the case, however. I've included a sample test app below, which basically sums all the integers within an object array - but the array contains a lot of null references and string references as well as boxed integers. The benchmark measures the code you'd have to use in C# 1, the code using the "as" operator, and just for kicks a LINQ solution. To my astonishment, the C# 1 code is 20 times faster in this case - and even the LINQ code (which I'd have expected to be slower, given the iterators involved) beats the "as" code.

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

Is the .NET implementation of isinstfor nullable types just really slow? Is it the additional unbox.anythat causes the problem? Is there another explanation for this? At the moment it feels like I'm going to have to include a warning against using this in performance sensitive situations...

isinst可空类型的 .NET 实现真的很慢吗?是额外unbox.any导致问题吗?对此还有其他解释吗?目前感觉我将不得不在性能敏感的情况下警告不要使用它......

Results:

结果:

Cast: 10000000 : 121
As: 10000000 : 2211
LINQ: 10000000 : 2143

演员表:10000000:121
作为:10000000:2211
LINQ:10000000:2143

Code:

代码:

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

采纳答案by Hans Passant

Clearly the machine code the JIT compiler can generate for the first case is much more efficient. One rule that really helps there is that an object can only be unboxed to a variable that has the same type as the boxed value. That allows the JIT compiler to generate very efficient code, no value conversions have to be considered.

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

The isoperator test is easy, just check if the object isn't null and is of the expected type, takes but a few machine code instructions. The cast is also easy, the JIT compiler knows the location of the value bits in the object and uses them directly. No copying or conversion occurs, all machine code is inline and takes but about a dozen instructions. This needed to be really efficient back in .NET 1.0 when boxing was common.

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

Casting to int? takes a lot more work. The value representation of the boxed integer is not compatible with the memory layout of Nullable<int>. A conversion is required and the code is tricky due to possible boxed enum types. The JIT compiler generates a call to a CLR helper function named JIT_Unbox_Nullable to get the job done. This is a general purpose function for any value type, lots of code there to check types. And the value is copied. Hard to estimate the cost since this code is locked up inside mscorwks.dll, but hundreds of machine code instructions is likely.

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

The Linq OfType() extension method also uses the isoperator and the cast. This is however a cast to a generic type. The JIT compiler generates a call to a helper function, JIT_Unbox() that can perform a cast to an arbitrary value type. I don't have a great explanation why it is as slow as the cast to Nullable<int>, given that less work ought to be necessary. I suspect that ngen.exe might cause trouble here.

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

回答by James Black

I don't have time to try it, but you may want to have:

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

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

as

作为

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

You are creating a new object each time, which won't completely explain the problem, but may contribute.

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

回答by Dirk Vollmar

It seems to me that the isinstis just really slow on nullable types. In method FindSumWithCastI changed

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

if (o is int)

to

if (o is int?)

which also significantly slows down execution. The only differenc in IL I can see is that

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

isinst     [mscorlib]System.Int32

gets changed to

更改为

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

回答by Marc Gravell

Interestingly, I passed on feedback about operator support via dynamicbeing an order-of-magnitude slower for Nullable<T>(similar to this early test) - I suspect for very similar reasons.

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

Gotta love Nullable<T>. Another fun one is that even though the JIT spots (and removes) nullfor non-nullable structs, it borks it for 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;
    }
}

回答by Michael Buen

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

Outputs:

输出:

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]

[编辑:2010-06-19]

Note: Previous test was done inside VS, configuration debug, using VS2009, using Core i7(company development machine).

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

The following was done on my machine using Core 2 Duo, using VS2010

以下是在我的机器上使用 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

回答by dalo

I tried the exact type check construct

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

typeof(int) == item.GetType(), which performs as fast as the item is intversion, and always returns the number (emphasis: even if you wrote a Nullable<int>to the array, you would need to use typeof(int)). You also need an additional null != itemcheck here.

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

However

然而

typeof(int?) == item.GetType()stays fast (in contrast to item is int?), but always returns false.

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

The typeof-construct is in my eyes the fastest way for exacttype checking, as it uses the RuntimeTypeHandle. Since the exact types in this case don't match with nullable, my guess is, is/ashave to do additional heavylifting here on ensuring that it is in fact an instance of a Nullable type.

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

And honestly: what does your is Nullable<xxx> plus HasValuebuy you? Nothing. You can always go directly to the underlying (value) type (in this case). You either get the value or "no, not an instance of the type you were asking for". Even if you wrote (int?)nullto the array, the type check will return false.

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

回答by Michael Buen

This is the result of FindSumWithAsAndHas above: alt text

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

This is the result of FindSumWithCast: alt text

这是 FindSumWithCast 的结果: 替代文字

Findings:

发现:

  • Using as, it test first if an object is an instance of Int32; under the hood it is using isinst Int32(which is similar to hand-written code: if (o is int) ). And using as, it also unconditionally unbox the object. And it's a real performance-killer to call a property(it's still a function under the hood), IL_0027

  • Using cast, you test first if object is an intif (o is int); under the hood this is using isinst Int32. If it is an instance of int, then you can safely unbox the value, IL_002D

  • 使用as,它首先测试一个对象是否是 Int32 的实例;它正在使用的引擎盖下isinst Int32(类似于手写代码: if (o is int) )。并且使用as,它也无条件地取消装箱对象。调用属性是一个真正的性能杀手(它仍然是引擎盖下的函数),IL_0027

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

Simply put, this is the pseudo-code of using asapproach:

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

int? x;

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

if (x.HasValue)
    sum += x.Value;    

And this is the pseudo-code of using cast approach:

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

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

So the cast ((int)a[i], well the syntax looks like a cast, but it's actually unboxing, cast and unboxing share the same syntax, next time I'll be pedantic with the right terminology) approach is really faster, you only needed to unbox a value when an object is decidedly an int. The same thing can't be said to using an asapproach.

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

回答by Michael Buen

Profiling further:

进一步分析:

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

}

Output:

输出:

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

What can we infer from these figures?

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

  • First, is-then-cast approach is significantly faster than asapproach. 303 vs 3524
  • Second, .Value is marginally slower than casting. 3524 vs 3272
  • Third, .HasValue is marginally slower than using manual has(i.e. using is). 3524 vs 3282
  • Fourth, doing an apple-to-apple comparison(i.e. both assigning of simulated HasValue and converting simulated Value happens together) between simulated asand real asapproach, we can see simulated asis still significantly faster than real as. 395 vs 3524
  • Lastly, based on first and fourth conclusion, there's something wrong with asimplementation ^_^
  • 首先,is-then-cast 方法比as方法快得多。303 对 3524
  • 其次, .Value 比强制转换稍慢。3524 对 3272
  • 第三, .HasValue 比使用手动 has(即使用is)稍微慢一些。3524 对 3282
  • 第四,在simulated asreal as方法之间进行apple-to-apple 比较(即分配模拟HasValue 和转换模拟值同时发生),我们可以看到simulated as仍然比real as快得多。395 对 3524
  • 最后,基于第一和第四个结论,有一些错误实现^ _ ^

回答by Johannes Rudolph

This originally started out as a Comment to Hans Passant's excellent answer, but it got too long so I want to add a few bits here:

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

First, the C# asoperator will emit an isinstIL instruction (so does the isoperator). (Another interesting instruction is castclass, emited when you do a direct cast and the compiler knows that runtime checking cannot be ommited.)

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

Here is what isinstdoes (ECMA 335 Partition III, 4.6):

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

Format: isinsttypeTok

typeTokis a metadata token (a typeref, typedefor typespec), indicating the desired class.

If typeTokis a non-nullable value type or a generic parameter type it is interpreted as “boxed” typeTok.

If typeTokis a nullable type, Nullable<T>, it is interpreted as “boxed” T

格式:isinst typeTok

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

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

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

Most importantly:

最重要的是:

If the actual type (not the verifier tracked type) of objis verifier-assignable-tothe type typeTok then isinstsucceeds and obj(as result) is returned unchanged while verification tracks its type as typeTok. Unlike coercions (§1.6) and conversions (§3.27), isinstnever changes the actual type of an object and preserves object identity (see Partition I).

如果obj的实际类型(不是验证器跟踪的类型)是验证器可分配给typeTok 类型的,则isinst成功并且obj(作为result)返回不变,而验证将其类型跟踪为typeTok与强制(第 1.6 节)和转换(第 3.27 节)不同,isinst永远不会更改对象的实际类型并保留对象身份(请参阅第 I 部分)。

So, the performance killer isn't isinstin this case, but the additional unbox.any. This wasn't clear from Hans' answer, as he looked at the JITed code only. In general, the C# compiler will emit an unbox.anyafter a isinst T?(but will omit it in case you do isinst T, when Tis a reference type).

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

Why does it do that? isinst T?never has the effect that would have been obvious, i.e. you get back a T?. Instead, all these instructions ensure is that you have a "boxed T"that can be unboxed to T?. To get an actual T?, we still need to unbox our "boxed T"to T?, which is why the compiler emits an unbox.anyafter isinst. If you think about it, this makes sense because the "box format" for T?is just a "boxed T"and making castclassand isinstperform the unbox would be inconsistent.

为什么这样做?isinst T?永远不会产生明显的效果,即您返回一个T?. 相反,所有这些说明确保您有一个"boxed T"可以拆箱到T?. 为了获得一个实际的T?,我们仍然需要拆箱我们"boxed T"T?,这就是为什么编译器发出unbox.anyisinst。如果你仔细想想,这是有道理的,因为“盒子格式”T?只是一个,"boxed T"而制作castclassisinst执行拆箱将是不一致的。

Backing up Hans' finding with some information from the standard, here it goes:

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

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

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

When applied to the boxed form of a value type, the unbox.anyinstruction extracts the value contained within obj (of type O). (It is equivalent to unboxfollowed by ldobj.) When applied to a reference type, the unbox.anyinstruction has the same effect as castclasstypeTok.

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

(ECMA 335 Partition III, 4.32): unbox

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

Typically, unboxsimply computes the address of the value type that is already present inside of the boxed object. This approach is not possible when unboxing nullable value types. Because Nullable<T>values are converted to boxed Tsduring the box operation, an implementation often must manufacture a new Nullable<T>on the heap and compute the address to the newly allocated object.

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

回答by Glenn Slayden

In order to keep this answer up-to-date, it's worth mentioning that the most of the discussion on this page is now moot now with C# 7.1and .NET 4.7which supports a slim syntax that also produces the best IL code.

为了使此答案保持最新,值得一提的是,此页面上的大部分讨论现在都没有实际意义,因为C# 7.1.NET 4.7支持可生成最佳 IL 代码的纤细语法。

The OP's original example...

OP的原始示例...

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

becomes simply...

变得简单...

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

I have found that one common use for the new syntax is when you are writing a .NET value type(i.e. structin C#) that implements IEquatable<MyStruct>(as most should). After implementing the strongly-typed Equals(MyStruct other)method, you can now gracefully redirect the untyped Equals(Object obj)override (inherited from Object) to it as follows:

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

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

 

 



Appendix:The Releasebuild ILcode for the first two example functions shown above in this answer (respectively) are given here. While the IL code for the new syntax is indeed 1 byte smaller, it mostly wins big by making zero calls (vs. two) and avoiding the unboxoperation altogether when possible.

附录:此处给出了本答案(分别)中显示的前两个示例函数的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

For further testing which substantiates my remark about the performance of the new C#7syntax surpassing the previously available options, see here(in particular, example 'D').

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