表演惊喜"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”代码快。

.NET对可空类型的isinst的实现真的很慢吗?是额外的unbox.any导致了这个问题吗?还有其他解释吗?目前,我觉得我必须在对性能敏感的情况下使用此功能时提出警告……

结果:

Cast: 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);
}
}
35405 次浏览

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

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

作为

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

您每次都在创建一个新对象,这不能完全解释问题,但可能有助于解决问题。

在我看来,isinst在可空类型上的速度真的很慢。在方法FindSumWithCast中我改变了

if (o is int)

if (o is int?)

这也大大降低了执行速度。我能看到的IL的唯一不同是

isinst     [mscorlib]System.Int32

变为

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

有趣的是,我通过dynamic传递了关于操作符支持的反馈,它比Nullable<T>慢一个数量级(类似于早期的测试)——我怀疑原因非常相似。

我爱上了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;
}
}
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)

注意:之前的测试是在VS内部完成的,配置调试,使用VS2009,使用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

我尝试了准确的类型检查结构

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

然而

typeof(int?) == item.GetType()保持快速(与item is int?相反),但总是返回false。

typef -construct在我看来是确切的类型检查的最快方式,因为它使用了RuntimeTypeHandle。由于这种情况下的确切类型与nullable不匹配,我的猜测是,is/as必须在这里做额外的重载,以确保它实际上是nullable类型的实例。

老实说:你的is Nullable<xxx> plus HasValue给你买了什么?没什么。您总是可以直接转到基础(值)类型(在本例中)。你要么得到值,要么“不,不是你要的类型的实例”。即使你在数组中写入了(int?)null,类型检查也会返回false。

这是FindSumWithAsAndHas上面的结果:alt text

这是FindSumWithCast的结果:alt text

发现:

  • 使用as,它首先测试对象是否是Int32的实例;在底层,它使用isinst Int32(类似于手写的代码:if (o is int))。并且使用as,它也无条件地解封对象。调用属性IL_0027是真正的性能杀手(本质上它仍然是一个函数)

  • 使用强制转换,首先测试对象是否为int if (o is int);在引擎盖下面,它使用isinst Int32。如果它是int的实例,那么可以安全地将值IL_002D解箱

简单地说,这是使用as方法的伪代码:

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)

所以强制转换((int)a[i],语法看起来像强制转换,但实际上是解装箱,强制转换和解装箱具有相同的语法,下次我会用正确的术语来卖弄)方法真的更快,你只需要在对象明确为int时解装箱。使用as方法就不能这么说了。

显然,JIT编译器为第一种情况生成的机器代码要高效得多。一个真正有用的规则是,一个对象只能被解箱为与被装箱值具有相同类型的变量。这允许JIT编译器生成非常高效的代码,不需要考虑值转换。

操作符测试很简单,只需检查对象是否为空并且是预期的类型,只需要一些机器代码指令。强制转换也很容易,JIT编译器知道值位在对象中的位置,并直接使用它们。没有复制或转换,所有的机器代码都是内联的,只需要大约十几个指令。这需要在。net 1.0中非常有效,当时装箱很常见。

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

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

进一步分析:

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
  • 其次,. value略慢于强制转换。3524 vs 3272
  • 第三,. hasvalue比使用手动has略慢。使用)。3524 vs 3282
  • 第四,做一个苹果与苹果的比较。在模拟,真正的是方法中,模拟HasValue的赋值和模拟值的转换都同时发生),我们可以看到模拟,仍然明显快于真正的是。395对3524
  • 最后,根据第一个和第四个结论,as有问题 李实现^ _ ^ < / >

这最初是对汉斯·帕桑的精彩回答的评论,但它太长了,所以我想在这里添加一些内容:

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

下面是isinst所做的(ECMA 335分区III, 4.6):

格式:isinst typeTok

typeTok是一个元数据令牌(typereftypedeftypespec),表示所需的类。

如果typeTok是非空值类型或泛型参数类型,则将其解释为“盒装”typeTok

如果typeTok是一个可空类型Nullable<T>,它将被解释为“盒装”的T

最重要的是:

如果obj的实际类型(不是验证者跟踪的类型)是verifier-assignable-to类型typeTok,则isinst成功,并且obj(作为结果)不变地返回,而验证者跟踪其类型为typeTok与强制(§1.6)和转换(§3.27)不同,isinst永远不会改变对象的实际类型,并保留对象的标识(见分区I)。

因此,在这种情况下,性能杀手不是isinst,而是额外的unbox.any。从汉斯的回答来看,这一点并不清楚,因为他只看了JITed代码。一般来说,c#编译器会在isinst T?之后发出unbox.any(但当T是引用类型时,如果你执行isinst T,则会省略它)。

为什么会这样?isinst T?永远不会有明显的效果,即你得到了一个T?。相反,所有这些指令都确保你有一个"boxed T",可以拆箱到T?。为了得到一个实际的T?,我们仍然需要将"boxed T"拆箱到T?,这就是为什么编译器在isinst之后会发出一个unbox.any。如果你仔细想想,这是有道理的,因为T?的“盒子格式”只是一个"boxed T",让T?1和isinst执行开箱操作是不一致的。

标准中的一些信息来支持Hans的发现,如下所示:

(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只是计算已经存在于装箱对象中的值类型的地址。当解装箱可空值类型时,这种方法是不可能的。因为在box操作期间,Nullable<T>值被转换为装箱的Ts,实现通常必须在堆上生成一个新的Nullable<T>,并计算新分配对象的地址。

为了保持这个答案是最新的,值得一提的是,本页上的大部分讨论现在都是无意义的,因为c# 7.1net 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 值类型(即c#中的struct)时(大多数人都应该这样)。在实现强类型的Equals(MyStruct other)方法之后,你现在可以优雅地将非类型的Equals(Object obj)重写(继承自Object)重定向到它,如下所示:

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

,


附录:这里给出了上述答案中(分别)显示的前两个示例函数的Release构建伊尔代码。虽然新语法的IL代码确实小了1个字节,但它主要是通过进行0次调用(而不是2次)并尽可能避免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.s x
call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
brfalse.s L_001e
ldarg.1
ldloca.s x
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

有关进一步的测试,以证实我关于新C # 7语法优于先前可用选项的性能的评论,请参阅在这里(特别是示例'D')。