性能惊喜与“为”和可为空的types

我只是修改了C#深入处理可空types的第4章,我添加了一个关于使用“as”运算符的部分,它允许您编写:

object o = ...; int? x = o as int?; if (x.HasValue) { ... // Use x.Value in here } 

我认为这是非常简洁的,它可以提高C#1的性能,使用“is”后跟一个cast – 毕竟,这样我们只需要dynamictypes检查一次,然后进行简单的值检查。

但是,这似乎并非如此。 我已经在下面包含了一个示例testing应用程序,它基本上总结了一个对象数组中的所有整数 – 但是数组包含了大量的空引用和string引用以及盒装整数。 该基准测量了您在C#1中使用的代码,使用“as”运算符的代码,以及用于踢LINQ解决scheme的代码。 令我惊讶的是,在这种情况下,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编译器可以为第一种情况生成的机器代码效率更高。 一个真正有帮助的规则是,一个对象只能被拆箱到与盒装值具有相同types的variables。 这允许JIT编译器生成非常有效的代码,不需要考虑值转换。

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

铸造到int? 需要更多的工作。 盒装整数的值表示与Nullable<int>的内存布局不兼容。 转换是必需的,由于可能的盒装枚举types,代码是棘手的。 JIT编译器生成对名为JIT_Unbox_Nullable的CLR帮助程序函数的调用,以完成作业。 这是任何值types的通用函数,有大量的代码来检查types。 并且该值被复制。 很难估计成本,因为这个代码被locking在mscorwks.dll中,但可能有数百个机器代码指令。

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

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

 if (o is int) 

 if (o is int?) 

这也大大减缓执行。 IL中唯一的区别是我能看到的是

 isinst [mscorlib]System.Int32 

变成了

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

这最初是作为对Hans Passant的出色答案的一个评论而开始的,但是它太长了,所以我想在这里添加一些:

首先,C# as操作符将发出isinst IL指令( is操作符is如此)。 (另一个有趣的指令是castclass ,当你进行直接的转换,并且编译器知道运行时检查不能被忽略的时候。

这是什么( ECMA 335 Partition III,4.6 ):

格式: isinst typeTok

typeTok是元数据标记( typereftypedeftypespec ),指示所需的类。

如果typeTok是非空值types或generics参数types,则将其解释为“boxed” typeTok

如果typeTok是一个可为空的types, Nullable<T> ,则它被解释为“boxed” T

最重要的是:

如果obj的实际types(不是validation器跟踪types)是可validation者可分配的typestypeTok,则isinst成功,并且obj结果 )返回不变,而validation将其types作为typeTok进行跟踪与强制(§1.6)和转换(§3.27)不同, isinst不会更改对象的实际types并保留对象标识(请参阅分区I)。

所以,性能杀手不是在这种情况下,而是额外的unbox.any 。 汉斯的回答并不清楚,因为他只看了JITed的代码。 一般来说,C#编译器会在isinst T?之后发出一个unbox.any isinst T? (但是,如果你是isinst TT是引用types,将省略它)。

为什么这样做? isinst T? 从来没有明显的效果,即你得到一个T? 。 相反,所有这些说明确保你有一个"boxed T"可以拆箱T? 。 要得到一个实际的T? ,我们仍然需要将我们的"boxed T"T? 这就是编译器在unbox.any之后发出unbox.any isinst 。 如果你仔细想想,这是有道理的,因为T?的“盒子格式” T? 只是一个"boxed T" ,使castclassisinst执行unbox将是不一致的。

用标准中的一些信息来支持汉斯的发现,这里是:

(ECMA 335 Partition III,4.33): unbox.any

当应用于值types的盒装forms时, unbox.any指令提取obj( Otypes)中包含的值。 (相当于unbox后跟ldobj 。)应用于引用types时, unbox.any指令与unbox.any具有相同的效果。

(ECMA 335 Partition III,4.32): unbox

通常, unbox只是计算已经存在于装箱对象内的值types的地址。 拆开可空值types时,这种方法是不可能的。 由于Nullable<T>值在盒子操作期间被转换为盒装Ts ,因此实现通常必须在堆上生成新的Nullable<T>并计算新分配的对象的地址。

有趣的是,我通过Nullable<T> (类似于这个早期testing )的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; } } 

这是上面FindSumWithAsAndHas的结果: alt text http://www.freeimagehosting.net/uploads/9e3c0bfb75.png

这是FindSumWithCast的结果: alt text http://www.freeimagehosting.net/uploads/ce8a5a3934.png

发现:

  • 使用as ,它首先testing一个对象是否是Int32的一个实例; 底下是使用isinst Int32 (类似于手写代码:if(o是int))。 并且使用它,也无条件地解开对象。 这是一个真正的性能杀手,调用一个属性(它仍然是一个function),IL_0027

  • 使用强制转换,如果对象是int if (o is int)首先testingif (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) 

所以cast( (int)a[i] ,语法看起来像是一个强制转换,但实际上是拆箱,cast和unboxing共享相同的语法,下一次我会用正确的术语迂腐)方法是更快,当一个对象是一个int时,你只需要取消一个值。 同样的事情不能用as方法来说。

进一步分析:

 using System; using System.Diagnostics; class Program { const int Size = 30000000; static void Main(string[] args) { object[] values = new object[Size]; for (int i = 0; i < Size - 2; i += 3) { values[i] = null; values[i + 1] = ""; values[i + 2] = 1; } FindSumWithIsThenCast(values); FindSumWithAsThenHasThenValue(values); FindSumWithAsThenHasThenCast(values); FindSumWithManualAs(values); FindSumWithAsThenManualHasThenValue(values); Console.ReadLine(); } static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithAsThenHasThenCast(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { int? x = o as int?; if (x.HasValue) { sum += (int)o; } } sw.Stop(); Console.WriteLine("As then Has then Cast: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithManualAs(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { bool hasValue = o is int; int x = hasValue ? (int)o : 0; if (hasValue) { sum += x; } } sw.Stop(); Console.WriteLine("Manual As: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithAsThenManualHasThenValue(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { int? x = o as int?; if (o is int) { sum += x.Value; } } sw.Stop(); Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } } 

输出:

 Is then Cast: 10000000 : 303 As then Has then Value: 10000000 : 3524 As then Has then Cast: 10000000 : 3272 Manual As: 10000000 : 395 As then Manual Has then Value: 10000000 : 3282 

我们可以从这些数字推断出什么?

  • 首先,随后投票方法比方法快得多。 303 vs 3524
  • 第二,价值比铸造稍微慢一些。 3524 vs 3272
  • 第三,.HasValue比使用手动有一点慢(即使用is )。 3524 vs 3282
  • 第四,在模拟实际之间进行苹果对苹果的比较(即模拟HasValue的分配和转换模拟值的一起发生),我们可以看到模拟的仍然比实际的快得多。 395 vs 3524
  • 最后,根据第一个和第四个结论,执行^ _ ^有问题

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

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

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

你每次创build一个新的对象,这不会完全解释问题,但可能有助于。

我尝试了确切的types检查构造

typeof(int) == item.GetType() ,它的执行速度与item is int版本一样快,并且总是返回数字(重点是:即使你写了一个Nullable<int>给数组,你也需要使用typeof(int) )。 您还需要一个额外的null != item检查在这里。

然而

typeof(int?) == item.GetType()保持快速(相对于item is int? ),但始终返回false。

typeof构造在我眼中是确切types检查的最快方式,因为它使用RuntimeTypeHandle。 由于在这种情况下确切的types与可为空的不匹配,我的猜测是,在这里必须进行额外的重新提升,以确保它实际上是Nullabletypes的实例。

说实话:你is Nullable<xxx> plus HasValue买你什么? 没有。 你总是可以直接到底层(值)types(在这种情况下)。 你要么得到的价值或“不,不是你所要求的types的实例”。 即使你向数组写了(int?)null ,types检查也会返回false。

 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); FindSumWithAsAndHas(values); FindSumWithAsAndIs(values); FindSumWithIsThenAs(values); FindSumWithIsThenConvert(values); FindSumWithLinq(values); Console.ReadLine(); } 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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithAsAndIs(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { int? x = o as int?; if (o is int) { sum += x.Value; } } sw.Stop(); Console.WriteLine("As and Is: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithIsThenAs(object[] values) { // Apple-to-apple comparison with Cast routine above. // Using the similar steps in Cast routine above, // the AS here cannot be slower than Linq. Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { if (o is int) { int? x = o as int?; sum += x.Value; } } sw.Stop(); Console.WriteLine("Is then As: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithIsThenConvert(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { if (o is int) { int x = Convert.ToInt32(o); sum += x; } } sw.Stop(); Console.WriteLine("Is then Convert: {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); } } 

输出:

 Cast: 10000000 : 456 As and Has: 10000000 : 2103 As and Is: 10000000 : 2029 Is then As: 10000000 : 1376 Is then Convert: 10000000 : 566 LINQ: 10000000 : 1811 

[编辑:2010-06-19]

注意:以前的testing是在VS内部进行的,使用VS2009进行configurationdebugging,使用Core i7(公司开发机器)。

以下是使用Core 2 Duo在我的机器上完成的,使用VS2010

 Inside VS, Configuration: Debug Cast: 10000000 : 309 As and Has: 10000000 : 3322 As and Is: 10000000 : 3249 Is then As: 10000000 : 1926 Is then Convert: 10000000 : 410 LINQ: 10000000 : 2018 Outside VS, Configuration: Debug Cast: 10000000 : 303 As and Has: 10000000 : 3314 As and Is: 10000000 : 3230 Is then As: 10000000 : 1942 Is then Convert: 10000000 : 418 LINQ: 10000000 : 1944 Inside VS, Configuration: Release Cast: 10000000 : 305 As and Has: 10000000 : 3327 As and Is: 10000000 : 3265 Is then As: 10000000 : 1942 Is then Convert: 10000000 : 414 LINQ: 10000000 : 1932 Outside VS, Configuration: Release Cast: 10000000 : 301 As and Has: 10000000 : 3274 As and Is: 10000000 : 3240 Is then As: 10000000 : 1904 Is then Convert: 10000000 : 414 LINQ: 10000000 : 1936 

为了保持这个答案是最新的,值得一提的是,现在大部分关于这个页面的讨论都是在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 } 

我发现新语法的一个常见用法是当你正在编写实现IEquatable<MyStruct>的.NET 值types (即在C#中的 struct )(大多数情况下)。 在实现了强types的Equals(MyStruct other)方法之后,现在可以按照如下方式优雅地将无types的Equals(Object obj)覆盖(从ObjectinheritanceEquals(Object obj)redirect到它:

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

附录:这里给出了这个答案(分别)中前两个示例函数的Release版本IL代码。 虽然新的语法给出了稍微小一点的IL代码,但是它通常在零调用(相对于两个)的情况下赢得大,并且尽可能避免unbox操作。

 // 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.sx call instance bool [mscorlib]Nullable`1<int32>::get_HasValue() brfalse.s L_001e ldarg.1 ldloca.sx 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 

对于进一步的testing,这certificate了我对新C#7语法性能的评价超过了以前可用的选项,请参阅此处 (特别是示例“D”)。