性能与惊喜“为”和可空类型(Performance surprise with “as” and n

2019-08-19 18:17发布

我只是修改的C#第4章中深度可空类型的交易,而我加入了部分有关使用“为”操作符,它可以让你写:

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

我认为这是很整洁,而且它可以改善在C#1当量性能,用“是”,然后铸造 - 毕竟,这样我们只需要问的动态类型检查一次,然后一个简单的值检查。

这似乎并非如此,但是。 我在下面列出了样品的测试程序,它基本上概括对象数组内的所有整数 - 但阵列中含有大量的空引用和字符串引用以及盒装整数。 基准的措施,你不得不在C#1使用的代码,使用“as”运算符,只是踢一个LINQ解决方案的代码。 令我惊讶的是,C#1码是20倍在这种情况下更快 - 甚至LINQ代码(我会预计要慢一些,因为涉及的迭代器)击败“为”代码。

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

Answer 1:

清楚的机器代码的JIT编译器可以生成对于第一种情况是更有效的。 一个规则确实有助于有一个对象只能是拆箱到具有相同类型的装箱值的变量。 这使JIT编译器生成高效的代码,没有价值的转换必须要考虑。

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

铸造为int? 需要更多的工作。 盒装整数的值表示是不与存储器布局兼容Nullable<int> 。 A转换是必需的,该代码是棘手由于可能盒装枚举类型。 JIT编译器生成名为JIT_Unbox_Nullable把工作做了CLR辅助函数的调用。 这是任何值类型的通用功能,大量的代码那里检查类型。 和值将被复制。 很难估计成本,因为这些代码被锁定里面的Mscorwks.dll,但数以百计的机器代码指令是可能的。

LINQ的OfType()扩展方法也使用IS操作者和铸造。 然而,这是一个强制转换为泛型类型。 JIT编译器生成到一个辅助功能,JIT_Unbox()可以执行一个铸造成任意值类型的呼叫。 我没有很好的解释为什么作为投它是慢Nullable<int> ,因为较少的工作应该是必要的。 我怀疑ngen.exe这里可能引起麻烦。



Answer 2:

在我看来,该isinst只是对空类型很慢。 在方法FindSumWithCast我改变

if (o is int)

if (o is int?)

这也显著减慢执行。 在IL唯一differenc我能看到的是,

isinst     [mscorlib]System.Int32

被改为

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


Answer 3:

这最初开始了作为一个评论汉斯帕桑特的出色答卷,但它得到了太久,所以我想在这里补充几个位:

首先,C# as操作者将发出isinst IL指令(以便确实的is运营商)。 (另一个有趣的指令是castclass ,当你做一个直接投emited和编译器知道运行时检查不能中省略。)

下面是isinst做( ECMA 335分区III,4.6 ):

格式:isinst typeTok

typeTok是元数据令牌(一个typereftypedeftypespec ),表明所要求的类别。

如果typeTok是一个非空值类型或它被解释为“加框” typeTok一个通用的参数类型。

如果typeTok是可空类型, Nullable<T>它被解释为“加框” T

最重要的是:

如果obj的实际类型(未验证跟踪型)是验证可分配到的类型typeTok然后isinst成功和OBJ(作为结果 ),同时验证跟踪其类型为typeTok返回不变。 不同于强制转换(§1.6)和转换(§3.27), isinst永远不会改变的实际类型的对象并保留对象标识(请参阅分区I)。

因此,性能杀手不isinst在这种情况下,但附加unbox.any 。 这不是从汉斯的答案明确,他看着只有JIT编译的代码。 一般地,C#编译器将发出一个unbox.any一个后isinst T? (但会忽略它在你的情况下isinst T ,当T是引用类型)。

为什么会这样? isinst T? 从来没有那会是效果明显,即你得到一个T? 。 取而代之的是,所有这些指令确保的是,你有一个"boxed T" ,可以拆箱到T? 。 为了获得一个实际的T? ,我们仍然需要拆箱我们的"boxed T"T? ,这就是为什么编译器会发出unbox.anyisinst 。 如果你仔细想想,这是有道理的,因为“框格式”为T? 仅仅是一个"boxed T" ,使castclassisinst进行拆箱会出现不一致。

备份汉斯发现从一些资料的标准 ,这里有云:

(ECMA 335分区III,4.33): unbox.any

当应用到数值类型的盒装形式中, unbox.any指令提取包含(类型OBJ中的值O )。 (它相当于unbox随后ldobj 。)当施加到参考类型, unbox.any指令具有相同的效果castclass typeTok。

(ECMA 335分区III,4.32): unbox

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



Answer 4:

有趣的是,我关于操作者支撑反馈经由通过dynamic是顺序的数量级的速度较慢为Nullable<T>类似于这个早期测试我怀疑非常类似的原因- )。

爱是爱Nullable<T> 。 另一个有趣的一个是,即使JIT斑点(并移除) null不可为空的结构,它borks它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;
    }
}


Answer 5:

这是FindSumWithAsAndHas的上述结果: 替代文字http://www.freeimagehosting.net/uploads/9e3c0bfb75.png

这是FindSumWithCast的结果: 替代文字http://www.freeimagehosting.net/uploads/ce8a5a3934.png

发现:

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

所以投( (int)a[i]以及语法看起来像一个演员,但它实际上拆箱,演员和拆箱共享相同的语法,下一次我会迂腐与正确的术语)的做法实在是快,你只需要拆箱的值当对象是决然的int 。 同样的事情不能用一种说as方法。



Answer 6:

进一步剖析:

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

我们能从这些数字推断?

  • 首先,-然后浇铸方法是不是办法显著更快。 303 VS 3524
  • 其次,.value的稍高于铸造慢。 3524 VS 3272
  • 第三,.HasValue是稍微慢于使用手动具有(即,使用 )。 3524 VS 3282
  • 四,做一个苹果对苹果的比较(即模拟的HasValue两种分配和转换的模拟值发生一起) 作为模拟实际的方法之间,我们可以看到模拟为仍显著快于真实的 。 395 VS 3524
  • 最后,基于第一和第四个结论,有一些错误实现^ _ ^


Answer 7:

我没有时间去尝试它,但你可能想有:

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

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

您正在创建一个新的对象,每次,这将不能完全说明问题,但可能有助于。



Answer 8:

我想确切的类型检查的构建

typeof(int) == item.GetType()它作为执行快速item is int版本,并始终返回数(强调:即使你写了一个Nullable<int>到阵列中,您将需要使用typeof(int) 您还需要额外的null != item在这里检查。

然而

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

typeof运算的构建是在我眼中的确切类型检查的最快方法,因为它使用的RuntimeTypeHandle。 因为在这种情况下,确切类型不可空匹配,我的猜测是, is/as要在这里做更多的heavylifting于确保它实际上是一个可空类型的实例。

和诚实:什么是你is Nullable<xxx> plus HasValue买吗? 没有。 您可以随时直接到底层(值)类型(在这种情况下)。 你要么获得的价值或“不,不是你问的类型的实例”。 即使你写的(int?)null到数组中,类型检查将返回false。



Answer 9:

为了保持这个答案了最新的,这是值得一提的是此页面上的大部分讨论都是现在用C#7.1和支持苗条的语法,也能产生最佳的IL代码.NET 4.7,现在没有实际意义。

该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”)。



Answer 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,采用酷睿i7(公司开发机)。

下面是使用酷睿2在我的机器上完成,使用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


文章来源: Performance surprise with “as” and nullable types