“as”和可为空的类型带来惊喜

我只是修改了深入讨论可空类型的C#的第4章,并且添加了一个关于使用“as”运算符的部分,它允许您编写:

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

我认为这非常简洁,并且它可以提高C#1等效性能,使用“is”后跟一个强制转换 - 毕竟,这样我们只需要一次动态类型检查,然后进行简单的值检查。

然而,这似乎并非如此。 我已经在下面包含了一个示例测试应用程序,它基本上将一个对象数组中的所有整数加起来 - 但该数组包含大量空引用和字符串引用以及盒装整数。 该基准测量了您必须在C#1中使用的代码,使用“as”运算符的代码以及用于踢出LINQ解决方案的代码。 令我惊讶的是,在这种情况下,C#1代码的速度提高了20倍 - 甚至LINQ代码(由于涉及到迭代器,我预计它会变慢)会跳过“as”代码。

可执行文件isinst的.NET实现是否真的很慢? 是额外的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);
    }
}

显然,JIT编译器可以为第一种情况生成的机器代码效率更高。 一个确实有帮助的规则是,只能将对象拆箱到与盒装值具有相同类型的变量。 这允许JIT编译器生成非常高效的代码,不需要考虑任何值转换。

is操作符测试非常简单,只需检查对象是否为空并且是预期类型,只需要几条机器代码指令即可。 转换也很简单,JIT编译器知道对象中值位的位置并直接使用它们。 没有复制或转换发生,所有机器代码是内联的,并且需要大约十几条指令。 在.NET 1.0中,当拳击很常见时,这需要非常高效。

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

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


在我看来, isinst对于可空类型非常慢。 在方法FindSumWithCast我改变了

if (o is int)

if (o is int?)

这也显着降低了执行速度。 我能看到的唯一区别在于

isinst     [mscorlib]System.Int32

变成了

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

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

首先,C# as操作符将发出isinst IL指令( is操作符is如此)。 (另一个有趣的指令是castclass ,当你进行直接转换并且编译器知道运行时检查不能被忽略时候。

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

格式: isinst typeTok

typeTok是元数据标记( typereftypedeftypespec ),表示所需的类。

如果typeTok是一个不可为空的值类型或泛型参数类型,则它被解释为“boxed”typeTok。

如果typeTok是一个可为空的类型, Nullable<T> ,则它被解释为“boxed” T

最重要的是:

如果obj的实际类型(不是验证器跟踪类型)是可验证者可分配的类型typeTok,则isinst成功,并且obj(结果)返回不变,而验证将其类型作为typeTok进行跟踪。 与强制(§1.6)和转换(§3.27)不同, isinst不会更改对象的实际类型并保留对象标识(请参阅分区I)。

因此,性能杀手不isinst在这种情况下,但附加unbox.any 。 汉斯的回答并不清楚,因为他只看了JITed代码。 一般地,C#编译器将发出一个unbox.any一个后isinst T? (但如果你使用isinst T ,当T是引用类型时,将省略它)。

它为什么这样做? isinst T? 从来没有明显的效果,即你回到T? 。 相反,所有这些说明确保您有一个可以拆箱到T?"boxed T" T? 。 要得到一个实际的T? ,我们仍然需要将我们的"boxed T"T? 这就是编译器在unbox.any之后发出unbox.any isinst 。 如果你考虑一下,这是有道理的,因为T?的“盒子格式” T? 只是一个"boxed T" ,并使castclassisinst执行unbox将不一致。

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

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

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

(ECMA 335 Partition III,4.32): unbox

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

链接地址: http://www.djcxy.com/p/50037.html

上一篇: Performance surprise with "as" and nullable types

下一篇: What is the best text editor for web development?