我只修改了深度中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 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);
}
}
10 回答
在我看来,
isinst
在可空类型上真的很慢 . 方法FindSumWithCast
我改变了至
这也显着减慢了执行速度 . 我可以看到IL的唯一不同之处在于
变了
这最初是作为对Hans Passant的优秀答案的评论开始的,但它太长了所以我想在这里添加一些内容:
首先,C#
as
运算符将发出isinst
IL指令(is
运算符也是如此) . (另一个有趣的指令是castclass
,当你进行直接转换时发出,编译器知道不能省略运行时检查 . )这是
isinst
的作用(ECMA 335 Partition III, 4.6):最重要的是:
因此,在这种情况下,性能杀手不是
isinst
,而是额外的unbox.any
. 这不是't clear from Hans'答案,因为他只看了JITed代码 . 通常,C#编译器将在isinst T?
之后发出unbox.any
(但如果isinst T
,T
是引用类型,则会省略它) .为什么这样做?
isinst T?
永远不会有明显的效果,即你回来了T?
. 相反,所有这些说明确保您有"boxed T"
可以取消装箱T?
. 要获得实际的T?
,我们仍然需要将"boxed T"
解包为T?
,这就是编译器在isinst
之后发出unbox.any
的原因 . 如果你考虑一下,这是有道理的,因为"box format"对于T?
只是一个"boxed T"
并且使castclass
和isinst
执行unbox会不一致 .通过standard中的一些信息来支持汉斯的发现,这里有:
(ECMA 335 Partition III,4.33):
unbox.any
(ECMA 335 Partition III,4.32):
unbox
显然,JIT编译器可以为第一种情况生成的机器代码更加高效 . 一个真正有用的规则是,对象只能拆箱到与盒装值具有相同类型的变量 . 这允许JIT编译器生成非常有效的代码,不必考虑任何值转换 .
运算符测试很简单,只需检查对象是否为空且是否为预期类型,只需要几个机器代码指令 . 转换也很简单,JIT编译器知道对象中值位的位置并直接使用它们 . 没有复制或转换,所有机器代码都是内联的,只需要十几条指令 . 当拳击很常见时,这需要在.NET 1.0中真正有效 .
投射到int?需要做更多的工作 . 盒装整数的值表示与
Nullable<int>
的内存布局不兼容 . 由于可能的盒装枚举类型,因此需要进行转换并且代码很棘手 . JIT编译器生成对名为JIT_Unbox_Nullable的CLR帮助函数的调用,以完成工作 . 这是任何值类型的通用函数,有很多代码用于检查类型 . 并且值被复制 . 很难估计成本,因为此代码被锁定在mscorwks.dll中,但很可能有数百条机器代码指令 .Linq OfType()扩展方法也使用了is运算符和强制转换 . 然而,这是对通用类型的强制转换 . JIT编译器生成对辅助函数JIT_Unbox()的调用,该函数可以执行强制转换为任意值类型 . 我没有一个很好的解释,为什么它和演员一样慢,因为应该做的工作少 . 我怀疑ngen.exe可能会在这里引起麻烦 .
有趣的是,我通过
dynamic
传递了关于操作员支持的反馈,对于Nullable<T>
(类似于this early test)来说是一个数量级的慢 - 我怀疑是非常相似的原因 .得爱
Nullable<T>
. 另一个有趣的是,即使JIT发现(并删除)null
用于非可空结构,它也会为Nullable<T>
进行borks:这是上面的FindSumWithAsAndHas的结果:alt text http://www.freeimagehosting.net/uploads/9e3c0bfb75.png
这是FindSumWithCast的结果:alt text http://www.freeimagehosting.net/uploads/ce8a5a3934.png
发现:
as
,它首先测试一个对象是否是Int32的一个实例;在引擎盖下它使用isinst Int32
(类似于手写代码:if(o是int)) . 并使用as
,它也无条件地取消对象的对象 . 并且它仍然是一个功能,IL_0027int
if (o is int)
,则首先进行测试;在引擎盖下,这是使用isinst Int32
. 如果它是int的实例,那么您可以安全地取消装箱值IL_002D简单地说,这是使用
as
方法的伪代码:这是使用强制转换方法的伪代码:
因此,演员(
(int)a[i]
,语法看起来像一个演员,但是它具有正确的术语迂腐)方法真的更快,你只需要在一个对象肯定是int
时取消装箱 . 同样的事情不能说使用as
方法 .进一步剖析:
输出:
我们可以从这些数字中推断出什么?
首先,is-then-cast方法明显快于 as 方法 . 303 vs 3524
其次,.Value比铸造慢一点 . 3524对3272
第三,.HasValue略微慢于使用手动(即使用 is ) . 3524对3282
第四,在 simulated as 和 real as 方法之间做一个苹果到苹果的比较(即分配模拟的HasValue和转换模拟值一起发生),我们可以看到 simulated as 仍然明显快于 real as . 395对3524
最后,根据第一和第四个结论, as 实现^ _ ^有问题
我没时间尝试,但你可能想要:
如
您每次都在创建一个新对象,这不会完全解释问题,但可能会有所帮助 .
我尝试了确切的类型检查构造
typeof(int) == item.GetType()
,执行速度与item is int
版本一样快,并且始终返回数字(强调:即使您向数组写了Nullable<int>
,也需要使用typeof(int)
) . 您还需要在此处进行额外的null != item
检查 .然而
typeof(int?) == item.GetType()
保持快速(与item is int?
形成对比),但始终返回false .在我看来,typeof-construct是精确类型检查的最快方式,因为它使用RuntimeTypeHandle . 由于这种情况下的确切类型与可空的不匹配,我的猜测是,
is/as
必须在这里进行额外的重量提升,以确保它实际上是Nullable类型的实例 .老实说:你的
is Nullable<xxx> plus HasValue
给你带来了什么?没有 . 您始终可以直接转到底层(值)类型(在本例中) . 你要么得到 Value ,要么"no, not an instance of the type you were asking for" . 即使您将(int?)null
写入数组,类型检查也将返回false .输出:
[EDIT: 2010-06-19]
注意:先前的测试是在VS,配置调试中使用VS2009,使用Core i7(公司开发机器)完成的 .
使用VS2010,使用Core 2 Duo在我的机器上完成以下操作
为了使这个答案保持最新,值得一提的是,这个页面上的大部分讨论现在都没有 C# 7.1 和 .NET 4.7 ,它支持一种纤细的语法,同时也产生了最好的IL代码 .
OP的原始例子......
变得简单......
我发现新语法的一个常见用途是当你编写实现
IEquatable<MyStruct>
的.NET value type (即struct
, C# )时(大多数应该) . 实现强类型Equals(MyStruct other)
方法后,您现在可以优雅地将无类型的Equals(Object obj)
覆盖(从Object
继承)重定向到它,如下所示:Appendix: 此处给出了本答案中分别显示的前两个示例函数的
Release
build IL 代码 . 虽然新语法的IL代码确实小了1个字节,但它通常通过进行零调用(相对于两个)并在可能时完全避免unbox
操作来获胜 .有关进一步测试证实了我对新 C#7 语法的性能超过以前可用选项的评论,请参阅here(特别是示例'D') .