“as”和可为空的类型带来惊喜
我只是修改了深入讨论可空类型的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”代码。
可执行文件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);
}
}
显然,JIT编译器可以为第一种情况生成的机器代码效率更高。 一个确实有帮助的规则是,只能将对象拆箱到与盒装值具有相同类型的变量。 这允许JIT编译器生成非常高效的代码,不需要考虑任何值转换。
is操作符测试非常简单,只需检查对象是否为空并且是预期类型,只需要几条机器代码指令即可。 转换也很简单,JIT编译器知道对象中值位的位置并直接使用它们。 没有复制或转换发生,所有机器代码是内联的,并且需要大约十几条指令。 在.NET 1.0中,当拳击很常见时,这需要非常高效。
投射到int? 需要更多的工作。 盒装整数的值表示与Nullable<int>
的内存布局不兼容。 转换是必需的,由于可能的盒装枚举类型,代码非常棘手。 JIT编译器生成对名为JIT_Unbox_Nullable的CLR辅助函数的调用,以完成工作。 对于任何值类型而言,这是一个通用函数,有很多代码用于检查类型。 并且该值被复制。 由于该代码被锁定在mscorwks.dll中,因此很难估计成本,但可能会有数百条机器代码指令。
Linq OfType()扩展方法也使用is运算符和cast。 然而,这是一种泛型类型转换。 JIT编译器生成对辅助函数JIT_Unbox()的调用,该函数可以执行强制转换为任意值类型。 我没有一个很好的解释,为什么它和Nullable<int>
一样慢,因为应该减少工作量。 我怀疑ngen.exe可能会在这里造成麻烦。
在我看来, isinst
对于可空类型非常慢。 在方法FindSumWithCast
我改变了
if (o is int)
至
if (o is int?)
这也显着降低了执行速度。 我能看到的唯一区别在于
isinst [mscorlib]System.Int32
变成了
isinst valuetype [mscorlib]System.Nullable`1<int32>
这最初是作为Hans Passant的出色答案的评论而开始的,但它太长了,所以我想在这里添加一些内容:
首先,C# as
操作符将发出isinst
IL指令( is
操作符is
如此)。 (另一个有趣的指令是castclass
,当你进行直接转换并且编译器知道运行时检查不能被忽略时候。
下面是isinst
做(ECMA 335分区III,4.6):
格式: isinst typeTok
typeTok是元数据标记( typeref
, typedef
或typespec
),表示所需的类。
如果typeTok是一个不可为空的值类型或泛型参数类型,则它被解释为“boxed”typeTok。
如果typeTok是一个可为空的类型, Nullable<T>
,则它被解释为“boxed” T
最重要的是:
如果obj的实际类型(不是验证器跟踪类型)是可验证者可分配的类型typeTok,则isinst
成功,并且obj(结果)返回不变,而验证将其类型作为typeTok进行跟踪。 与强制(§1.6)和转换(§3.27)不同, isinst
不会更改对象的实际类型并保留对象标识(请参阅分区I)。
因此,性能杀手不isinst
在这种情况下,但附加unbox.any
。 汉斯的回答并不清楚,因为他只看了JITed代码。 一般地,C#编译器将发出一个unbox.any
一个后isinst T?
(但如果你使用isinst T
,当T
是引用类型时,将省略它)。
它为什么这样做? isinst T?
从来没有明显的效果,即你回到T?
。 相反,所有这些说明确保您有一个可以拆箱到T?
的"boxed T"
T?
。 要得到一个实际的T?
,我们仍然需要将我们的"boxed T"
到T?
这就是编译器在unbox.any
之后发出unbox.any
isinst
。 如果你考虑一下,这是有道理的,因为T?
的“盒子格式” T?
只是一个"boxed T"
,并使castclass
和isinst
执行unbox将不一致。
用标准中的一些信息来支持汉斯的发现,这里有:
(ECMA 335 Partition III,4.33): unbox.any
当应用于值类型的装箱形式时, unbox.any
指令提取obj(类型O
)中包含的值。 (它相当于unbox
随后ldobj
。)当施加到参考类型, unbox.any
指令具有相同的效果castclass
typeTok。
(ECMA 335 Partition III,4.32): unbox
通常, unbox
仅计算已存在于装箱对象内的值类型的地址。 拆开可空值类型时,这种方法是不可能的。 因为Nullable<T>
值在盒子操作期间被转换为盒装Ts
,所以实现通常必须在堆上生成新的Nullable<T>
并计算新分配的对象的地址。