我只是修改了深度中C#的第4章处理可空类型,我正在添加一个关于使用“as”运算符的部分,它允许你写:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
我认为这非常简洁,它可以提高C#1等效性能,使用“is”后跟一个演员 - 毕竟,这样我们只需要请求动态类型检查一次,然后a简单的价值检查。
然而,情况似乎并非如此。我在下面包含了一个示例测试应用程序,它基本上对对象数组中的所有整数求和 - 但该数组包含许多空引用和字符串引用以及盒装整数。该基准测试您必须在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);
}
}
答案 0 :(得分:201)
显然,JIT编译器可以为第一种情况生成的机器代码效率更高。一个真正有用的规则是,只能将对象取消装箱到与盒装值具有相同类型的变量。这允许JIT编译器生成非常有效的代码,不必考虑任何值转换。
是运算符测试很简单,只需检查对象是否为空并且是否为预期类型,只需要一些机器代码指令。转换也很简单,JIT编译器知道对象中值位的位置并直接使用它们。没有复制或转换,所有机器代码都是内联的,只需要十几条指令。当拳击很常见时,这需要在.NET 1.0中真正有效。
转换为int?需要做更多的工作。盒装整数的值表示与Nullable<int>
的内存布局不兼容。由于可能的盒装枚举类型,需要进行转换并且代码很棘手。 JIT编译器生成对名为JIT_Unbox_Nullable的CLR帮助函数的调用,以完成工作。这是任何值类型的通用函数,有很多代码用于检查类型。并且值被复制。很难估计成本,因为此代码被锁定在mscorwks.dll中,但很可能有数百条机器代码指令。
Linq OfType()扩展方法也使用是运算符和强制转换。然而,这是对通用类型的强制转换。 JIT编译器生成对辅助函数JIT_Unbox()的调用,该函数可以执行强制转换为任意值类型。我没有一个很好的解释,为什么它与Nullable<int>
的演员一样慢,因为应该做的工作量较少。我怀疑ngen.exe可能会在这里造成麻烦。
答案 1 :(得分:26)
在我看来,isinst
在可空类型上真的很慢。在方法FindSumWithCast
中,我更改了
if (o is int)
到
if (o is int?)
这也显着减慢了执行速度。我能看到的IL中唯一不同的是
isinst [mscorlib]System.Int32
变为
isinst valuetype [mscorlib]System.Nullable`1<int32>
答案 2 :(得分:22)
这最初是对Hans Passant的优秀答案的评论开始,但它太长了所以我想在这里添加一些内容:
首先,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 的实际类型(不是验证者跟踪类型)是 verifier-assignable-to 类型typeTok,那么
isinst
成功并且 obj (作为 result )不会更改,而验证会将其类型跟踪为 typeTok 。 与强制(§1.6)和转换(§3.27)不同,isinst
永远不会更改对象的实际类型并保留对象标识(请参阅分区I)。
因此,在这种情况下,性能杀手不是isinst
,而是额外的unbox.any
。汉斯的回答并不清楚,因为他只看了JITed代码。通常,C#编译器将在unbox.any
之后发出isinst T?
(但如果isinst T
为T
,则isinst T?
将会忽略它。)
为什么这样做? T?
永远不会有明显的效果,即你回来"boxed T"
。相反,所有这些说明都确保您有T?
可以取消装箱到T?
。要获得实际的"boxed T"
,我们仍然需要将T?
取消装箱到unbox.any
,这就是编译器在isinst
之后发出T?
的原因。如果您考虑一下,这是有道理的,因为"boxed T"
的“框格式”只是castclass
而使isinst
和unbox.any
执行unbox会不一致。< / p>
使用standard中的一些信息来支持Hans的发现,这里是:
(ECMA 335 Partition III,4.33):unbox.any
当应用于值类型的盒装形式时,
O
指令将提取obj(类型unbox
)中包含的值。 (相当于ldobj
后跟unbox.any
。)当应用于引用类型时,castclass
指令与unbox
typeTok具有相同的效果。
(ECMA 335 Partition III,4.32):unbox
通常,
Nullable<T>
只计算已装箱对象内已存在的值类型的地址。取消装箱可以为空的值类型时,这种方法是不可行的。因为在框操作期间Ts
值被转换为盒装Nullable<T>
,所以实现通常必须在堆上制造新的{{1}}并计算新分配的对象的地址。
答案 3 :(得分:19)
有趣的是,我通过dynamic
传递了有关运营商支持的反馈,Nullable<T>
的速度比Nullable<T>
慢(类似于this early test) - 我怀疑原因非常相似。< / p>
得爱null
。另一个有趣的问题是,即使JIT针对不可为空的结构发现(并删除)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;
}
}
点缀它:
{{1}}
答案 4 :(得分:12)
这是上面的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_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
方法也不能说同样的事情。
答案 5 :(得分:9)
进一步剖析:
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
我们可以从这些数字中推断出什么?
答案 6 :(得分:8)
我没时间尝试,但你可能想要:
foreach (object o in values)
{
int? x = o as int?;
作为
int? x;
foreach (object o in values)
{
x = o as int?;
您每次都在创建一个新对象,这不会完全解释问题,但可能会有所帮助。
答案 7 :(得分:8)
我尝试了确切的类型检查构造
typeof(int) == item.GetType()
,其执行速度与item is int
版本一样快,并且始终返回数字(强调:即使您向数组写了Nullable<int>
,也需要使用{ {1}})。您还需要在此处进行额外的typeof(int)
检查。
然而
null != item
保持快速(与typeof(int?) == item.GetType()
相反),但始终返回false。
typeof-construct在我眼中是精确类型检查的最快方式,因为它使用RuntimeTypeHandle。由于这种情况下的确切类型与nullable不匹配,我的猜测是,item is int?
必须在此确保它实际上是Nullable类型的实例。
老实说:你is/as
买了什么?没有。您始终可以直接转到基础(值)类型(在本例中)。您要么获得值,要么“不,不是您要求的类型的实例”。即使您将is Nullable<xxx> plus HasValue
写入数组,类型检查也将返回false。
答案 8 :(得分:8)
为了使这个答案保持最新,值得一提的是,此页面上的大部分讨论现在都没有用 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);
附录:此处给出了本答案中上面显示的前两个示例函数的Release
构建 IL 代码。虽然新语法的IL代码确实小了1个字节,但它通常通过进行零调用(相对于两个)并在可能的情况下完全避免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 语法的性能超过之前可用选项的评论,请参阅here(特别是示例“D”)。< / p>
答案 9 :(得分:7)
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