我只是在修改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>,并计算新分配对象的地址。

其他回答

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

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

爱上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;
    }
}

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

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#的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>,并计算新分配对象的地址。

这是上面FindSumWithAsAndHas的结果:

这是FindSumWithCast的结果:

发现:

使用as,它首先测试对象是否是Int32的实例;实际上,它使用isinst Int32(这类似于手写的代码:if (o is int))。使用as,它也无条件地打开对象。调用属性IL_0027是真正的性能杀手(本质上它仍然是一个函数) 使用强制转换,首先测试object是否为int型if (o is int);这是在使用isinst Int32。如果它是int的实例,那么可以安全地将值IL_002D解箱

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

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方法。