我只是在修改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# 7.1和。net 4.7来说,本页上的大部分讨论现在都是毫无意义的,因为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值类型(即c#中的struct)实现IEquatable<MyStruct>(大多数应该)。在实现强类型Equals(MyStruct other)方法之后,你现在可以优雅地重定向非类型Equals(Object obj)覆盖(继承自Object)到它,如下所示:

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

 


附录:在这个答案中给出的前两个示例函数的发布构建IL代码(分别)在这里。虽然新语法的IL代码确实少了1个字节,但它主要是通过不调用(而不是两个)并尽可能避免开箱操作来获得最大的胜利。

// 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')。

其他回答

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

if (o is int)

to

if (o is int?)

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

isinst     [mscorlib]System.Int32

变为

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

有趣的是,对于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;
    }
}

为了使这个答案保持最新,值得一提的是,对于c# 7.1和。net 4.7来说,本页上的大部分讨论现在都是毫无意义的,因为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值类型(即c#中的struct)实现IEquatable<MyStruct>(大多数应该)。在实现强类型Equals(MyStruct other)方法之后,你现在可以优雅地重定向非类型Equals(Object obj)覆盖(继承自Object)到它,如下所示:

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

 


附录:在这个答案中给出的前两个示例函数的发布构建IL代码(分别)在这里。虽然新语法的IL代码确实少了1个字节,但它主要是通过不调用(而不是两个)并尽可能避免开箱操作来获得最大的胜利。

// 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')。

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

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

as

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

您每次都在创建一个新对象,这不能完全解释问题,但可能有助于解决问题。

这是上面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方法。