我只是在修改C#中有关可空类型的第4章,并添加了有关使用“ as”运算符的部分,该节允许您编写:

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


我认为这真的很整洁,并且可以使用“ is”和强制类型转换来提高C#1等效项的性能-毕竟,这种方式我们只需要进行一次动态类型检查,并且然后进行简单的值检查。

但事实并非如此。我在下面提供了一个示例测试应用程序,该应用程序基本上将对象数组中的所有整数相加-但该数组包含许多空引用和字符串引用以及装箱的整数。基准测试可测量您在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);
    }
}


评论

为什么不看一下固定代码?甚至VS调试器也可以显示它。

我很好奇,您也使用CLR 4.0进行了测试吗?

@安东:好点。会在某个时候做(尽管目前在VS中还没有:) @divo:是的,而且整个情况都更糟。但这就是beta版本,因此其中可能包含许多调试代码。

今天,我了解到您可以在可为空的类型上使用as。有趣,因为它不能在其他值类型上使用。其实更令人惊讶。

@Lepp对于不使用值类型非常有意义。考虑一下,尝试转换为类型,如果失败,则返回null。您不能将值类型设置为null

#1 楼

显然,JIT编译器可以为第一种情况生成的机器代码效率更高。一个真正有帮助的规则是,对象只能被取消装箱到与装箱值具有相同类型的变量。这样,JIT编译器就可以生成非常高效的代码,而不必考虑值的转换。只需要一些机器代码指令。转换也很容易,JIT编译器知道值位在对象中的位置并直接使用它们。没有复制或转换发生,所有机器代码都是内联的,只需要十几条指令。装箱很普遍时,这在.NET 1.0中必须非常有效。需要做更多的工作。装箱整数的值表示形式与Nullable<int>的内存布局不兼容。需要进行转换,并且由于可能的装箱枚举类型不同,因此代码很棘手。 JIT编译器生成对名为JIT_Unbox_Nullable的CLR帮助器函数的调用,以完成工作。这是用于任何值类型的通用函数,其中有许多代码可以检查类型。并复制该值。由于此代码已锁定在mscorwks.dll中,因此难以估算成本,但是可能会出现数百条机器代码指令。

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

评论


好吧,我坚信。我想我曾经认为“ is”可能是昂贵的,因为有可能沿继承层次结构前进-但是在值类型的情况下,就没有层次结构的可能性,因此可以进行简单的按位比较。我仍然认为,对于可为空的情况,JIT代码可以比现在进行更多优化。

–乔恩·斯基特(Jon Skeet)
2010-6-19 18:21

#2 楼

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

if (o is int)

更改为

if (o is int?)


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

isinst     [mscorlib]System.Int32


被更改为

评论


不仅如此;在“ cast”情况下,在isinst之后进行无效性测试,然后有条件地进行unbox.any。在可为空的情况下,存在一个无条件的unbox.any。

–乔恩·斯基特(Jon Skeet)
09-10-17在20:14

是的,事实证明isinst和unbox.any在可空类型上都比较慢。

–德克·沃尔玛(Dirk Vollmar)
09-10-17在20:26

@Jon:您可以查看我的答案,以了解为什么需要演员表。 (我知道这很旧,但是我刚刚发现了这个q并认为我应该提供有关CLR的2c信息)。

–约翰内斯·鲁道夫(Johannes Rudolph)
2011年8月17日在20:24



#3 楼

最初,这是对Hans Passant出色答案的评论,但是它太长了,因此我想在此处添加一些内容:

首先,C#as运算符将发出isinst IL指令(因此执行is运算符)。 (另一个有趣的指令是castclass,当您执行直接强制转换并且编译器知道不能忽略运行时检查时发出。)

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


格式:isinst typeTok

typeTok是元数据令牌(typereftypedeftypespec),指示所需的类。

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

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

最重要的是:如果obj的实际类型(不是验证者跟踪的类型)是verifier-assignedable-typeTok类型,则T成功,并且验证跟踪其类型为typeTok时,obj(作为结果)保持不变。与强制(§1.6)和转换(§3.27)不同,isinst永远不会更改对象的实际类型并保留对象标识(请参阅分区I)。


因此,性能杀手isn在这种情况下不是isinst,而是其他isinst。从Hans的回答中并不清楚,因为他只看了JITed代码。通常,C#编译器会在unbox.any之后发出unbox.any(但如果isinst T?是引用类型,则在执行isinst T时会省略它)。为什么这样做呢? T永远不会具有明显的效果,即您可以返回isinst T?。取而代之的是,所有这些说明确保您拥有一个可以拆箱到T?"boxed T"。要获得实际的T?,我们仍然需要将T?拆箱到"boxed T",这就是为什么编译器在T?之后发出unbox.any的原因。如果您考虑一下,这是有道理的,因为isinst的“框格式”仅是T?,而使"boxed T"castclass执行取消框操作将是不一致的。标准,就可以了:

(ECMA 335 Partition III,4.33):isinst

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


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


通常,castclass只是计算装箱对象内部已经存在的值类型的地址。将可空值类型装箱时,这种方法是不可能的。由于在框操作期间unbox的值会转换为带框的unbox,因此实现通常必须在堆上制造新的Nullable<T>并计算到新分配的对象的地址。

评论


我认为最后引用的句子可能有错别字;不应“ ...在堆...上”是“在执行堆栈上?”好像取消装箱回到一些新的GC堆实例中,原来的问题换成了几乎相同的新问题。

– Glenn Slayden
19 Mar 2 '19 at 5:38

#4 楼

有趣的是,我通过dynamic传递了有关操作员支持的反馈,该反馈对于Nullable<T>的响应速度较慢(类似于此早期测试)-我怀疑原因非常相似。另一个有趣的事情是,即使JIT为非空结构都发现(并删除了)Nullable<T>,但它却使null陷入困境:

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


评论


尤瑟那真是一个痛苦的区别。真是的

–乔恩·斯基特(Jon Skeet)
09-10-17在21:32

如果所有这些都没有其他好处,那就让我在原始代码和代码中都包含警告:)

–乔恩·斯基特(Jon Skeet)
09-10-18在11:52

我知道这是一个古老的问题,但是您能否解释一下“对于不可为空的结构,JIT发现(并删除)null”是什么意思?您是说它在运行时将null替换为默认值或其他值吗?

–贾斯汀·摩根(Justin Morgan)
2011年4月21日在21:23



@Justin-泛型方法可以在运行时使用任何数量的泛型参数(T等)排列。堆栈等的要求取决于args(局部堆栈的堆栈空间等),因此对于涉及值类型的任何唯一置换,您都会获得一个JIT。但是,引用的大小都相同,因此共享一个JIT。在执行按值类型的JIT时,它可以检查一些明显的情况,并尝试消除由于无法实现空值之类的不可达代码。请注意,这并不完美。另外,我也忽略了上述的AOT。

– Marc Gravell♦
2011年4月21日在21:32

无限制的可空测试仍然要慢2.5个数量级,但是当您不使用count变量时,会进行一些优化。添加Console.Write(count.ToString()+“”);在watch.Stop()之后;在这两种情况下,其他测试的速度都降低了一个数量级,但不受限制的可空测试不会更改。请注意,在测试传递null的情况时,也会发生变化,以确认原始代码并未真正对其他测试进行null检查和递增。 Linqpad

–马克·赫德
2014年5月20日6:01



#5 楼

为了使此答案保持最新,值得一提的是,此页面上的大部分讨论现在都在使用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
}


我发现,新语法的一种常见用法是在编写实现struct(大多数情况下)的.NET值类型(即C#中的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

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

#6 楼

这是上述FindSumWithAsAndHas的结果:


这是FindSumWithCast的结果:


发现:


使用as,它将首先测试对象是否是Int32的实例;在引擎盖下使用的是isinst Int32(类似于手写代码:if(o is int))。并使用as,也可以无条件地将对象拆箱。调用属性(它仍然是一个函数)是一个真正的性能杀手,IL_0027
使用强制转换,首先测试对象是否为int。在引擎盖下使用if (o is int)。如果它是int的实例,则可以放心地将值IL_002D

简单地拆箱,这是使用isinst Int32方法的伪代码:

int? x;

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

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


这是使用强制转换方法的伪代码:实际上,拆箱,强制转换和拆箱使用相同的语法,下次我将使用正确的术语进行学步)方法确实更快,只需要在对象绝对是as时拆开值即可。使用(int)a[i]方法不能说同一件事。

#7 楼

进一步进行分析:


首先,“先浇铸”方法要比“浇铸”方法更快。 303 vs 3524
第二。.value比强制转换稍慢。 3524 vs 3272
第三,.HasValue比使用手动has稍慢(即使用is)。 3524 vs 3282
第四,在模拟方法和真实方法之间进行一个苹果对苹果的比较(即同时分配模拟的HasValue和转换模拟的值),我们可以看到模拟方法仍然比真实方法更快如。 395 vs 3524
最后,基于第一个和第四个结论,as
实现有问题[^ _ ^


#8 楼

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

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

评论


不,我跑过了,速度慢了一点。

–汉克·霍尔特曼
09-10-17在20:03

在我的经验中,在不同位置声明变量只会对捕获到的变量产生重大影响(此时会影响实际的语义)。请注意,尽管它肯定是在创建int的新实例,但并未在堆上创建新的对象?使用unbox.any在堆栈上。我怀疑这就是问题所在-我的猜测是,手工制作的IL可能在这里击败了这两种选择...虽然JIT也有可能经过优化以识别is / cast情况,并且只检查一次。

–乔恩·斯基特(Jon Skeet)
09-10-17在20:08

我一直在考虑演员表的优化,因为它已经存在了很长时间。

–詹姆斯·布莱克(James Black)
09-10-17在20:17

is / cast是优化的一个简单目标,这是一个令人讨厌的惯用语。

– Anton Tykhyy
09-10-17在20:26

创建方法的堆栈框架时,将在堆栈上分配局部变量,因此在方法中声明变量的位置完全没有区别。 (除非它当然处于关闭状态,但是这里不是这种情况。)

–古法
09-10-17在21:30

#9 楼

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

typeof(int) == item.GetType(),其执行速度与item is int版本一样快,并且始终返回数字(强调:即使您将Nullable<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。

评论


有趣的是...使用“ as” + HasValue(不是加上HasValue,请注意)的想法是,它只执行一次类型检查,而不是两次。它一步完成“检查并取消装箱”。感觉应该更快一些……但事实并非如此。我不确定最后一句话是什么意思,但是没有盒装整数吗? -如果您将int装箱?值,其结果将以盒装int或null引用结束。

–乔恩·斯基特(Jon Skeet)
2010年6月19日在10:17

#10 楼

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(公司开发机器)进行配置调试。

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


评论


您不感兴趣使用的是哪个框架版本?我的上网本(使用.NET 4RC)上的结果更加引人注目-使用As的版本比您的结果差得多。也许他们已经对.NET 4 RTM进行了改进?我仍然认为这可能会更快...

–乔恩·斯基特(Jon Skeet)
2010年4月21日在6:06

@Michael:您正在运行未优化的版本,还是在调试器中运行?

–乔恩·斯基特(Jon Skeet)
2010年6月19日,9:10

@Jon:未优化的构建,在调试器下

–迈克尔·布恩(Michael Buen)
2010年6月19日,9:52

@Michael:对-我倾向于将调试器下的性能结果视为无关紧要的:)

–乔恩·斯基特(Jon Skeet)
2010年6月19日,9:53

@Jon:如果是在调试器下进行,则表示在VS内部;是的,以前的基准测试是在调试器下完成的。我再次在VS内部和外部进行基准测试,并分别编译为调试版本和发布版本。检查编辑

–迈克尔·布恩(Michael Buen)
2010年6月19日上午10:19