“as”带来的性能惊喜和可为 null 的类型

发布于 2024-08-07 14:54:09 字数 2346 浏览 13 评论 0原文

我只是在深入修改 C# 的第 4 章,其中涉及可为 null 的类型,并且我添加了有关使用“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);
    }
}

I'm just revising chapter 4 of C# in Depth which deals with nullable types, and I'm adding a section about using the "as" operator, which allows you to write:

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

I thought this was really neat, and that it could improve performance over the C# 1 equivalent, using "is" followed by a cast - after all, this way we only need to ask for dynamic type checking once, and then a simple value check.

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.

Is the .NET implementation of isinst for nullable types just really slow? Is it the additional unbox.any that causes the problem? Is there another explanation for this? At the moment it feels like I'm going to have to include a warning against using this in performance sensitive situations...

Results:

Cast: 10000000 : 121
As: 10000000 : 2211
LINQ: 10000000 : 2143

Code:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

若水微香 2024-08-14 14:54:09

显然,JIT 编译器可以为第一种情况生成的机器代码效率更高。一条真正有帮助的规则是,只能将对象拆箱为与装箱值具有相同类型的变量。这使得 JIT 编译器能够生成非常高效的代码,无需考虑值转换。

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

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

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

Clearly the machine code the JIT compiler can generate for the first case is much more efficient. One rule that really helps there is that an object can only be unboxed to a variable that has the same type as the boxed value. That allows the JIT compiler to generate very efficient code, no value conversions have to be considered.

The is operator test is easy, just check if the object isn't null and is of the expected type, takes but a few machine code instructions. The cast is also easy, the JIT compiler knows the location of the value bits in the object and uses them directly. No copying or conversion occurs, all machine code is inline and takes but about a dozen instructions. This needed to be really efficient back in .NET 1.0 when boxing was common.

Casting to int? takes a lot more work. The value representation of the boxed integer is not compatible with the memory layout of Nullable<int>. A conversion is required and the code is tricky due to possible boxed enum types. The JIT compiler generates a call to a CLR helper function named JIT_Unbox_Nullable to get the job done. This is a general purpose function for any value type, lots of code there to check types. And the value is copied. Hard to estimate the cost since this code is locked up inside mscorwks.dll, but hundreds of machine code instructions is likely.

The Linq OfType() extension method also uses the is operator and the cast. This is however a cast to a generic type. The JIT compiler generates a call to a helper function, JIT_Unbox() that can perform a cast to an arbitrary value type. I don't have a great explanation why it is as slow as the cast to Nullable<int>, given that less work ought to be necessary. I suspect that ngen.exe might cause trouble here.

悲念泪 2024-08-14 14:54:09

在我看来,isinst 对于可空类型来说确实很慢。在方法 FindSumWithCast 中,我更改

if (o is int)

为该

if (o is int?)

方法,这也显着减慢了执行速度。我能看到的 IL 中唯一的区别是它

isinst     [mscorlib]System.Int32

被更改为

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

It seems to me that the isinst is just really slow on nullable types. In method FindSumWithCast I changed

if (o is int)

to

if (o is int?)

which also significantly slows down execution. The only differenc in IL I can see is that

isinst     [mscorlib]System.Int32

gets changed to

isinst     valuetype [mscorlib]System.Nullable`1<int32>
坏尐絯 2024-08-14 14:54:09

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

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

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

格式:isinst typeTok

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

如果typeTok是不可空值类型或通用参数类型,则它被解释为“装箱”typeTok

如果typeTok是可空类型,Nullable,它被解释为“装箱”T

最重要的是:

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

因此,性能在这种情况下,killer 不是 isinst,而是附加的 unbox.any。汉斯的回答并不清楚这一点,因为他只查看了 JIT 代码。一般来说,C# 编译器会在 isinst T? 之后发出 unbox.any (但如果您执行 isinst T,则会忽略它,当 T 是引用类型时)。

为什么要这样做? isinst T? 永远不会产生明显的效果,即您返回一个 T?。相反,所有这些说明确保您拥有一个可以拆箱为 T?“装箱 T”。要获得实际的 T?,我们仍然需要将 "boxed T" 拆箱为 T?,这就是编译器发出 T? 的原因isinst 之后的代码>unbox.any。如果您考虑一下,这是有道理的,因为 T? 的“盒格式”只是一个 “盒装 T” 并使得 castclassisinst 执行拆箱会不一致。

使用标准<中的一些信息来支持汉斯的发现/a>,这里是:

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

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

(ECMA 335 分区 III,4.32):拆箱

通常,unbox 只是计算已存在于装箱对象内部的值类型的地址。取消装箱可为空值类型时,此方法不可行。由于 Nullable 值在装箱操作期间会转换为装箱 T,因此实现通常必须在堆并计算新分配对象的地址。

This originally started out as a Comment to Hans Passant's excellent answer, but it got too long so I want to add a few bits here:

First, the C# as operator will emit an isinst IL instruction (so does the is operator). (Another interesting instruction is castclass, emited when you do a direct cast and the compiler knows that runtime checking cannot be ommited.)

Here is what isinst does (ECMA 335 Partition III, 4.6):

Format: isinst typeTok

typeTok is a metadata token (a typeref, typedef or typespec), indicating the desired class.

If typeTok is a non-nullable value type or a generic parameter type it is interpreted as “boxed” typeTok.

If typeTok is a nullable type, Nullable<T>, it is interpreted as “boxed” T

Most importantly:

If the actual type (not the verifier tracked type) of obj is verifier-assignable-to the type typeTok then isinst succeeds and obj (as result) is returned unchanged while verification tracks its type as typeTok. Unlike coercions (§1.6) and conversions (§3.27), isinst never changes the actual type of an object and preserves object identity (see Partition I).

So, the performance killer isn't isinst in this case, but the additional unbox.any. This wasn't clear from Hans' answer, as he looked at the JITed code only. In general, the C# compiler will emit an unbox.any after a isinst T? (but will omit it in case you do isinst T, when T is a reference type).

Why does it do that? isinst T? never has the effect that would have been obvious, i.e. you get back a T?. Instead, all these instructions ensure is that you have a "boxed T" that can be unboxed to T?. To get an actual T?, we still need to unbox our "boxed T" to T?, which is why the compiler emits an unbox.any after isinst. If you think about it, this makes sense because the "box format" for T? is just a "boxed T" and making castclass and isinst perform the unbox would be inconsistent.

Backing up Hans' finding with some information from the standard, here it goes:

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

When applied to the boxed form of a value type, the unbox.any instruction extracts the value contained within obj (of type O). (It is equivalent to unbox followed by ldobj.) When applied to a reference type, the unbox.any instruction has the same effect as castclass typeTok.

(ECMA 335 Partition III, 4.32): unbox

Typically, unbox simply computes the address of the value type that is already present inside of the boxed object. This approach is not possible when unboxing nullable value types. Because Nullable<T> values are converted to boxed Ts during the box operation, an implementation often must manufacture a new Nullable<T> on the heap and compute the address to the newly allocated object.

握住我的手 2024-08-14 14:54:09

有趣的是,我通过 dynamic 传递了有关运算符支持的反馈,对于 Nullable 来说,速度要慢一个数量级(类似于 这个早期测试) - 我怀疑出于非常相似的原因。

一定喜欢 Nullable。另一个有趣的地方是,即使 JIT 发现(并删除)不可为空结构的 null,它也会为 Nullable 删除它:

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

Interestingly, I passed on feedback about operator support via dynamic being an order-of-magnitude slower for Nullable<T> (similar to this early test) - I suspect for very similar reasons.

Gotta love Nullable<T>. Another fun one is that even though the JIT spots (and removes) null for non-nullable structs, it borks it for 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;
    }
}
入画浅相思 2024-08-14 14:54:09

为了使这个答案保持最新,值得一提的是,此页面上的大部分讨论现在对于 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 值类型(即 structC#中)实现了IEquatable(大多数人应该这样做)。实现强类型 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”)。

In order to keep this answer up-to-date, it's worth mentioning that the most of the discussion on this page is now moot now with C# 7.1 and .NET 4.7 which supports a slim syntax that also produces the best IL code.

The OP's original example...

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

becomes simply...

if (o is int x)
{
    // ...use x in here
}

I have found that one common use for the new syntax is when you are writing a .NET value type (i.e. struct in C#) that implements IEquatable<MyStruct> (as most should). After implementing the strongly-typed Equals(MyStruct other) method, you can now gracefully redirect the untyped Equals(Object obj) override (inherited from Object) to it as follows:

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

 


Appendix: The Release build IL code for the first two example functions shown above in this answer (respectively) are given here. While the IL code for the new syntax is indeed 1 byte smaller, it mostly wins big by making zero calls (vs. two) and avoiding the unbox operation altogether when possible.

// 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

For further testing which substantiates my remark about the performance of the new C#7 syntax surpassing the previously available options, see here (in particular, example 'D').

罪歌 2024-08-14 14:54:09

这是上面 FindSumWithAsAndHas 的结果: 替代文字

这是 FindSumWithCast 的结果: 替代文字

结果:

  • 使用as,它首先测试一个对象是否是 Int32 的实例;它在底层使用 isinst Int32 (类似于手写代码: if (o is 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],好吧,语法看起来像强制转换,但实际上是拆箱,强制转换和拆箱共享相同的语法,下次我会迂腐地用右边的terminology) 方法确实更快,当对象明确是 int 时,您只需要取消装箱值。使用 as 方法却不能说同样的事情。

This is the result of FindSumWithAsAndHas above: alt text

This is the result of FindSumWithCast: alt text

Findings:

  • Using as, it test first if an object is an instance of Int32; under the hood it is using isinst Int32 (which is similar to hand-written code: if (o is int) ). And using as, it also unconditionally unbox the object. And it's a real performance-killer to call a property(it's still a function under the hood), IL_0027

  • Using cast, you test first if object is an int if (o is int); under the hood this is using isinst Int32. If it is an instance of int, then you can safely unbox the value, IL_002D

Simply put, this is the pseudo-code of using as approach:

int? x;

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

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

And this is the pseudo-code of using cast approach:

if (o isinst Int32)
    sum += (o unbox Int32)

So the cast ((int)a[i], well the syntax looks like a cast, but it's actually unboxing, cast and unboxing share the same syntax, next time I'll be pedantic with the right terminology) approach is really faster, you only needed to unbox a value when an object is decidedly an int. The same thing can't be said to using an as approach.

若沐 2024-08-14 14:54:09

进一步分析:

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

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

  • 首先,is-then-cast 方法明显比 as 方法快。 303 vs 3524
  • 其次,.Value 比铸造稍慢。 3524 vs 3272
  • 第三,.HasValue 比使用手动 has 稍微慢一些(即使用 is)。 3524 vs 3282
  • 第四,在模拟为真实为方法之间进行逐个比较(即模拟HasValue的分配和模拟值的转换同时发生),我们可以看到模拟为仍然明显快于真实为。 395 vs 3524
  • 最后,根据第一和第四个结论,as有问题
    实施^_^

Profiling further:

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

}

Output:

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

What can we infer from these figures?

  • 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 ^_^
节枝 2024-08-14 14:54:09

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

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

因为

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

您每次都创建一个新对象,这不会完全解释问题,但可能有所贡献。

I don't have time to try it, but you may want to have:

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

as

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

You are creating a new object each time, which won't completely explain the problem, but may contribute.

神经大条 2024-08-14 14:54:09

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

typeof(int) == item.GetType(),它的执行速度与 item is int 版本一样快,并且始终返回数字 (强调:即使您向数组写入了 Nullable,您也需要使用 typeof(int))。您还需要在此处进行额外的 null != item 检查。

然而,

typeof(int?) == item.GetType() 保持快速(与 item is int? 相比),但总是返回 false。

在我看来,typeof-construct 是进行精确类型检查的最快方法,因为它使用 RuntimeTypeHandle。由于本例中的确切类型与 nullable 不匹配,我的猜测是,is/as 必须在此处进行额外的繁重工作,以确保它实际上是 Nullable 类型的实例。

老实说:你的 is Nullable是什么?再加上HasValue买你的?没有什么。您始终可以直接转到底层(值)类型(在本例中)。您要么获得该值,要么“不,不是您所要求的类型的实例”。即使您将 (int?)null 写入数组,类型检查也会返回 false。

I tried the exact type check construct

typeof(int) == item.GetType(), which performs as fast as the item is int version, and always returns the number (emphasis: even if you wrote a Nullable<int> to the array, you would need to use typeof(int)). You also need an additional null != item check here.

However

typeof(int?) == item.GetType() stays fast (in contrast to item is int?), but always returns false.

The typeof-construct is in my eyes the fastest way for exact type checking, as it uses the RuntimeTypeHandle. Since the exact types in this case don't match with nullable, my guess is, is/as have to do additional heavylifting here on ensuring that it is in fact an instance of a Nullable type.

And honestly: what does your is Nullable<xxx> plus HasValue buy you? Nothing. You can always go directly to the underlying (value) type (in this case). You either get the value or "no, not an instance of the type you were asking for". Even if you wrote (int?)null to the array, the type check will return false.

说谎友 2024-08-14 14:54:09
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
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);
    }
}

Outputs:

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

[EDIT: 2010-06-19]

Note: Previous test was done inside VS, configuration debug, using VS2009, using Core i7(company development machine).

The following was done on my machine using Core 2 Duo, using 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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文