小编典典

“as”和可空类型的性能惊喜

all

我只是在深度修改 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”代码。

可空类型的 .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);
    }
}

阅读 95

收藏
2022-04-06

共1个答案

小编典典

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

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

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

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

2022-04-06