我只是在深度修改 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是导致问题的附加因素吗?对此还有其他解释吗?目前感觉我将不得不包含一个警告,不要在性能敏感的情况下使用它......
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); } }
显然,JIT 编译器可以为第一种情况生成的机器代码效率更高。一个真正有帮助的规则是,一个对象只能被拆箱为一个与装箱值具有相同类型的变量。这允许 JIT 编译器生成非常高效的代码,无需考虑值转换。
is 运算符测试很简单,只需检查对象是否为 null 且是否为预期类型,只需要一些机器代码指令即可。转换也很容易,JIT 编译器知道对象中值位的位置并直接使用它们。不会发生复制或转换,所有机器代码都是内联的,只需要大约十几个指令。当拳击很常见时,这需要在 .NET 1.0 中非常有效。
转换为int?需要更多的工作。装箱整数的值表示与 的内存布局不兼容Nullable<int>。需要进行转换,并且由于可能的盒装枚举类型,代码很棘手。JIT 编译器生成对名为 JIT_Unbox_Nullable 的 CLR 帮助函数的调用以完成工作。这是任何值类型的通用函数,那里有很多代码来检查类型。并且该值被复制。由于此代码被锁定在 mscorwks.dll 中,因此难以估计成本,但可能有数百条机器代码指令。
Nullable<int>
Linq OfType() 扩展方法也使用 is 运算符和强制转换。然而,这是对泛型类型的强制转换。JIT 编译器生成对辅助函数 JIT_Unbox() 的调用,该函数可以执行强制转换为任意值类型。我没有很好的解释为什么它和 cast to 一样慢Nullable<int>,因为应该需要更少的工作。我怀疑 ngen.exe 可能会在这里造成麻烦。