我只是在修改c#深度的第4章,它涉及到可空类型,我增加了一个关于使用“as”操作符的章节,它允许你这样写:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
我认为这真的很整洁,它可以比c# 1的等效方法提高性能,使用“is”后面加强制转换——毕竟,这样我们只需要要求进行一次动态类型检查,然后进行简单的值检查。
This appears not to be the case, however. I've included a sample test app below, which basically sums all the integers within an object array - but the array contains a lot of null references and string references as well as boxed integers. The benchmark measures the code you'd have to use in C# 1, the code using the "as" operator, and just for kicks a LINQ solution. To my astonishment, the C# 1 code is 20 times faster in this case - and even the LINQ code (which I'd have expected to be slower, given the iterators involved) beats the "as" code.
对于可空类型的isinst的。net实现真的很慢吗?是额外的开箱。是什么导致了这个问题?还有其他解释吗?目前,我觉得我必须在对性能敏感的情况下使用此功能时提出警告……
结果:
演员: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);
}
}
这最初是对汉斯·帕桑的精彩回答的评论,但它太长了,所以我想在这里添加一些内容:
首先,c#的as操作符将发出一条isinst IL指令(is操作符也是如此)。(另一个有趣的指令是castclass,在执行直接强制转换时发出,编译器知道不能省略运行时检查。)
以下是isinst的功能(ECMA 335 Partition III, 4.6):
格式:isinst typeTok
typeTok是一个元数据标记(typeref, typedef或typespec),指示所需的类。
如果typeTok是非空值类型或泛型参数类型,则将其解释为“盒装”typeTok。
如果typeTok是一个可空类型,nullable <T>,它被解释为“盒装”T
最重要的是:
如果obj的实际类型(不是验证者跟踪的类型)是valider -assignable-to typeTok类型,那么isinst成功,obj(作为结果)不变地返回,而验证者跟踪它的类型为typeTok。与强制(§1.6)和转换(§3.27)不同,isinst永远不会改变对象的实际类型,并保持对象的同一性(见分区I)。
因此,在本例中,性能杀手不是isinst,而是额外的unbox.any。从汉斯的回答来看,这一点并不清楚,因为他只看了JITed代码。一般来说,c#编译器会发出一个解框。isinst T后面有什么?(但当T是引用类型时,如果执行isinst T将省略它)。
为什么会这样?isinst T ?从来没有明显的效果,即你得到了一个T?相反,所有这些说明都确保您有一个“盒装T”,可以打开盒装T?得到一个真正的T?那么,我们还需要把我们的“盒装T”打开吗?,这就是为什么编译器会发出一个解框。任何在isinst之后。如果你仔细想想,这是有道理的,因为T的“盒子格式”?只是一个“被装箱的T”,让castclass和isinst执行unbox将是不一致的。
用标准中的一些信息来支持Hans的发现,下面是:
(ECMA 335 Partition III, 4.33): unbox.any
当应用于值类型的盒装形式时,解盒装。任何指令提取obj中包含的值(类型为O)。(相当于unbox后跟ldobj。)当应用于引用类型时,解框。任何指令都具有与castclass typeTok相同的效果。
(ECMA 335 Partition III, 4.32):开箱
通常,unbox只是计算已装箱对象中已经存在的值类型的地址。当解装箱可空值类型时,这种方法是不可能的。由于在box操作期间Nullable<T>值被转换为盒装的T,因此实现通常必须在堆上生成一个新的Nullable<T>,并计算新分配对象的地址。
这最初是对汉斯·帕桑的精彩回答的评论,但它太长了,所以我想在这里添加一些内容:
首先,c#的as操作符将发出一条isinst IL指令(is操作符也是如此)。(另一个有趣的指令是castclass,在执行直接强制转换时发出,编译器知道不能省略运行时检查。)
以下是isinst的功能(ECMA 335 Partition III, 4.6):
格式:isinst typeTok
typeTok是一个元数据标记(typeref, typedef或typespec),指示所需的类。
如果typeTok是非空值类型或泛型参数类型,则将其解释为“盒装”typeTok。
如果typeTok是一个可空类型,nullable <T>,它被解释为“盒装”T
最重要的是:
如果obj的实际类型(不是验证者跟踪的类型)是valider -assignable-to typeTok类型,那么isinst成功,obj(作为结果)不变地返回,而验证者跟踪它的类型为typeTok。与强制(§1.6)和转换(§3.27)不同,isinst永远不会改变对象的实际类型,并保持对象的同一性(见分区I)。
因此,在本例中,性能杀手不是isinst,而是额外的unbox.any。从汉斯的回答来看,这一点并不清楚,因为他只看了JITed代码。一般来说,c#编译器会发出一个解框。isinst T后面有什么?(但当T是引用类型时,如果执行isinst T将省略它)。
为什么会这样?isinst T ?从来没有明显的效果,即你得到了一个T?相反,所有这些说明都确保您有一个“盒装T”,可以打开盒装T?得到一个真正的T?那么,我们还需要把我们的“盒装T”打开吗?,这就是为什么编译器会发出一个解框。任何在isinst之后。如果你仔细想想,这是有道理的,因为T的“盒子格式”?只是一个“被装箱的T”,让castclass和isinst执行unbox将是不一致的。
用标准中的一些信息来支持Hans的发现,下面是:
(ECMA 335 Partition III, 4.33): unbox.any
当应用于值类型的盒装形式时,解盒装。任何指令提取obj中包含的值(类型为O)。(相当于unbox后跟ldobj。)当应用于引用类型时,解框。任何指令都具有与castclass typeTok相同的效果。
(ECMA 335 Partition III, 4.32):开箱
通常,unbox只是计算已装箱对象中已经存在的值类型的地址。当解装箱可空值类型时,这种方法是不可能的。由于在box操作期间Nullable<T>值被转换为盒装的T,因此实现通常必须在堆上生成一个新的Nullable<T>,并计算新分配对象的地址。
为了使这个答案保持最新,值得一提的是,对于c# 7.1和。net 4.7来说,本页上的大部分讨论现在都是毫无意义的,因为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
}
我发现新语法的一个常见用途是当你编写一个。net值类型(即c#中的struct)实现IEquatable<MyStruct>(大多数应该)。在实现强类型Equals(MyStruct other)方法之后,你现在可以优雅地重定向非类型Equals(Object obj)覆盖(继承自Object)到它,如下所示:
public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
附录:在这个答案中给出的前两个示例函数的发布构建IL代码(分别)在这里。虽然新语法的IL代码确实少了1个字节,但它主要是通过不调用(而不是两个)并尽可能避免开箱操作来获得最大的胜利。
// 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')。
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
显然,JIT编译器为第一种情况生成的机器代码要高效得多。一个真正有用的规则是,一个对象只能被解箱为与被装箱值具有相同类型的变量。这允许JIT编译器生成非常高效的代码,不需要考虑值转换。
is操作符测试很简单,只需检查对象是否为空并且是预期的类型,只需要一些机器代码指令。强制转换也很容易,JIT编译器知道值位在对象中的位置,并直接使用它们。没有复制或转换,所有的机器代码都是内联的,只需要大约十几个指令。这需要在。net 1.0中非常有效,当时装箱很常见。
Casting to int? takes a lot more work. The value representation of the boxed integer is not compatible with the memory layout of Nullable<int>. A conversion is required and the code is tricky due to possible boxed enum types. The JIT compiler generates a call to a CLR helper function named JIT_Unbox_Nullable to get the job done. This is a general purpose function for any value type, lots of code there to check types. And the value is copied. Hard to estimate the cost since this code is locked up inside mscorwks.dll, but hundreds of machine code instructions is likely.
Linq的OfType()扩展方法也使用is操作符和强制转换。然而,这是一个泛型类型的强制转换。JIT编译器生成对辅助函数JIT_Unbox()的调用,该函数可以执行到任意值类型的强制转换。我没有很好的解释为什么它像强制转换为Nullable<int>一样慢,因为应该需要更少的工作。我怀疑ngen.exe可能会在这里造成麻烦。