我只是在修改c#深度的第4章,它涉及到可空类型,我增加了一个关于使用“as”操作符的章节,它允许你这样写:

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

我认为这真的很整洁,它可以比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.

对于可空类型的isinst的。net实现真的很慢吗?是额外的开箱。是什么导致了这个问题?还有其他解释吗?目前,我觉得我必须在对性能敏感的情况下使用此功能时提出警告……

结果:

演员: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);
    }
}

当前回答

这最初是对汉斯·帕桑的精彩回答的评论,但它太长了,所以我想在这里添加一些内容:

首先,c#的as操作符将发出一条isinst IL指令(is操作符也是如此)。(另一个有趣的指令是castclass,在执行直接强制转换时发出,编译器知道不能省略运行时检查。)

以下是isinst的功能(ECMA 335 Partition III, 4.6):

格式:isinst typeTok typeTok是一个元数据标记(typeref, typedef或typespec),指示所需的类。 如果typeTok是非空值类型或泛型参数类型,则将其解释为“盒装”typeTok。 如果typeTok是一个可空类型,nullable <T>,它被解释为“盒装”T

最重要的是:

如果obj的实际类型(不是验证者跟踪的类型)是valider -assignable-to typeTok类型,那么isinst成功,obj(作为结果)不变地返回,而验证者跟踪它的类型为typeTok。与强制(§1.6)和转换(§3.27)不同,isinst永远不会改变对象的实际类型,并保持对象的同一性(见分区I)。

因此,在本例中,性能杀手不是isinst,而是额外的unbox.any。从汉斯的回答来看,这一点并不清楚,因为他只看了JITed代码。一般来说,c#编译器会发出一个解框。isinst T后面有什么?(但当T是引用类型时,如果执行isinst T将省略它)。

为什么会这样?isinst T ?从来没有明显的效果,即你得到了一个T?相反,所有这些说明都确保您有一个“盒装T”,可以打开盒装T?得到一个真正的T?那么,我们还需要把我们的“盒装T”打开吗?,这就是为什么编译器会发出一个解框。任何在isinst之后。如果你仔细想想,这是有道理的,因为T的“盒子格式”?只是一个“被装箱的T”,让castclass和isinst执行unbox将是不一致的。

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

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

当应用于值类型的盒装形式时,解盒装。任何指令提取obj中包含的值(类型为O)。(相当于unbox后跟ldobj。)当应用于引用类型时,解框。任何指令都具有与castclass typeTok相同的效果。

(ECMA 335 Partition III, 4.32):开箱

通常,unbox只是计算已装箱对象中已经存在的值类型的地址。当解装箱可空值类型时,这种方法是不可能的。由于在box操作期间Nullable<T>值被转换为盒装的T,因此实现通常必须在堆上生成一个新的Nullable<T>,并计算新分配对象的地址。

其他回答

我尝试了准确的类型检查结构

typeof(int) == item. gettype(),它的执行速度与项目为int版本一样快,并且总是返回数字(强调:即使你向数组写入了Nullable<int>,你也需要使用typeof(int))。这里还需要一个额外的null !=项检查。

然而

typeof(int?) == item. gettype()保持较快(相对于item is int?),但总是返回false。

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

老实说:你的is Nullable<xxx> + HasValue给你买了什么?没什么。您总是可以直接转到基础(值)类型(在本例中)。你要么得到值,要么“不,不是你要的类型的实例”。即使您将(int?)null写入数组,类型检查也将返回false。

为了使这个答案保持最新,值得一提的是,对于c# 7.1和。net 4.7来说,本页上的大部分讨论现在都是毫无意义的,因为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值类型(即c#中的struct)实现IEquatable<MyStruct>(大多数应该)。在实现强类型Equals(MyStruct other)方法之后,你现在可以优雅地重定向非类型Equals(Object obj)覆盖(继承自Object)到它,如下所示:

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

 


附录:在这个答案中给出的前两个示例函数的发布构建IL代码(分别)在这里。虽然新语法的IL代码确实少了1个字节,但它主要是通过不调用(而不是两个)并尽可能避免开箱操作来获得最大的胜利。

// 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')。

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

if (o is int)

to

if (o is int?)

这也大大降低了执行速度。我能看到的IL的唯一不同是

isinst     [mscorlib]System.Int32

变为

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

这最初是对汉斯·帕桑的精彩回答的评论,但它太长了,所以我想在这里添加一些内容:

首先,c#的as操作符将发出一条isinst IL指令(is操作符也是如此)。(另一个有趣的指令是castclass,在执行直接强制转换时发出,编译器知道不能省略运行时检查。)

以下是isinst的功能(ECMA 335 Partition III, 4.6):

格式:isinst typeTok typeTok是一个元数据标记(typeref, typedef或typespec),指示所需的类。 如果typeTok是非空值类型或泛型参数类型,则将其解释为“盒装”typeTok。 如果typeTok是一个可空类型,nullable <T>,它被解释为“盒装”T

最重要的是:

如果obj的实际类型(不是验证者跟踪的类型)是valider -assignable-to typeTok类型,那么isinst成功,obj(作为结果)不变地返回,而验证者跟踪它的类型为typeTok。与强制(§1.6)和转换(§3.27)不同,isinst永远不会改变对象的实际类型,并保持对象的同一性(见分区I)。

因此,在本例中,性能杀手不是isinst,而是额外的unbox.any。从汉斯的回答来看,这一点并不清楚,因为他只看了JITed代码。一般来说,c#编译器会发出一个解框。isinst T后面有什么?(但当T是引用类型时,如果执行isinst T将省略它)。

为什么会这样?isinst T ?从来没有明显的效果,即你得到了一个T?相反,所有这些说明都确保您有一个“盒装T”,可以打开盒装T?得到一个真正的T?那么,我们还需要把我们的“盒装T”打开吗?,这就是为什么编译器会发出一个解框。任何在isinst之后。如果你仔细想想,这是有道理的,因为T的“盒子格式”?只是一个“被装箱的T”,让castclass和isinst执行unbox将是不一致的。

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

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

当应用于值类型的盒装形式时,解盒装。任何指令提取obj中包含的值(类型为O)。(相当于unbox后跟ldobj。)当应用于引用类型时,解框。任何指令都具有与castclass typeTok相同的效果。

(ECMA 335 Partition III, 4.32):开箱

通常,unbox只是计算已装箱对象中已经存在的值类型的地址。当解装箱可空值类型时,这种方法是不可能的。由于在box操作期间Nullable<T>值被转换为盒装的T,因此实现通常必须在堆上生成一个新的Nullable<T>,并计算新分配对象的地址。

进一步分析:

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

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

First, is-then-cast approach is significantly faster than as approach. 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 as and real as approach, we can see simulated as is still significantly faster than real as. 395 vs 3524 Lastly, based on first and fourth conclusion, there's something wrong with as implementation ^_^