object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
我认为这真的很整洁,并且可以使用“ is”和强制类型转换来提高C#1等效项的性能-毕竟,这种方式我们只需要进行一次动态类型检查,并且然后进行简单的值检查。
但事实并非如此。我在下面提供了一个示例测试应用程序,该应用程序基本上将对象数组中的所有整数相加-但该数组包含许多空引用和字符串引用以及装箱的整数。基准测试可测量您在C#1中必须使用的代码,使用“ as”运算符的代码,仅用于启动LINQ解决方案。令我惊讶的是,在这种情况下,C#1代码的速度提高了20倍-甚至LINQ代码(考虑到迭代器,我本来希望它也更慢)击败了“ as”代码。 />可空类型的.NET实现
isinst
真的很慢吗?是引起问题的其他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);
}
}
#1 楼
显然,JIT编译器可以为第一种情况生成的机器代码效率更高。一个真正有帮助的规则是,对象只能被取消装箱到与装箱值具有相同类型的变量。这样,JIT编译器就可以生成非常高效的代码,而不必考虑值的转换。只需要一些机器代码指令。转换也很容易,JIT编译器知道值位在对象中的位置并直接使用它们。没有复制或转换发生,所有机器代码都是内联的,只需要十几条指令。装箱很普遍时,这在.NET 1.0中必须非常有效。需要做更多的工作。装箱整数的值表示形式与Nullable<int>
的内存布局不兼容。需要进行转换,并且由于可能的装箱枚举类型不同,因此代码很棘手。 JIT编译器生成对名为JIT_Unbox_Nullable的CLR帮助器函数的调用,以完成工作。这是用于任何值类型的通用函数,其中有许多代码可以检查类型。并复制该值。由于此代码已锁定在mscorwks.dll中,因此难以估算成本,但是可能会出现数百条机器代码指令。Linq OfType()扩展方法还使用is运算符和强制转换。但是,这是强制转换为通用类型。 JIT编译器会生成对辅助函数JIT_Unbox()的调用,该函数可以将类型转换为任意值类型。考虑到应该减少工作量,我没有很好的解释,为什么它和
Nullable<int>
一样慢。我怀疑ngen.exe可能会在这里引起麻烦。评论
好吧,我坚信。我想我曾经认为“ is”可能是昂贵的,因为有可能沿继承层次结构前进-但是在值类型的情况下,就没有层次结构的可能性,因此可以进行简单的按位比较。我仍然认为,对于可为空的情况,JIT代码可以比现在进行更多优化。
–乔恩·斯基特(Jon Skeet)
2010-6-19 18:21
#2 楼
在我看来,isinst
在可为空的类型上真的很慢。在方法FindSumWithCast
中,我将if (o is int)
更改为
if (o is int?)
,这也大大降低了执行速度。我能看到的IL的唯一区别是
isinst [mscorlib]System.Int32
被更改为
评论
不仅如此;在“ cast”情况下,在isinst之后进行无效性测试,然后有条件地进行unbox.any。在可为空的情况下,存在一个无条件的unbox.any。
–乔恩·斯基特(Jon Skeet)
09-10-17在20:14
是的,事实证明isinst和unbox.any在可空类型上都比较慢。
–德克·沃尔玛(Dirk Vollmar)
09-10-17在20:26
@Jon:您可以查看我的答案,以了解为什么需要演员表。 (我知道这很旧,但是我刚刚发现了这个q并认为我应该提供有关CLR的2c信息)。
–约翰内斯·鲁道夫(Johannes Rudolph)
2011年8月17日在20:24
#3 楼
最初,这是对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>
,则将其解释为“装箱的” > 最重要的是:如果obj的实际类型(不是验证者跟踪的类型)是verifier-assignedable-typeTok类型,则
T
成功,并且验证跟踪其类型为typeTok时,obj(作为结果)保持不变。与强制(§1.6)和转换(§3.27)不同,isinst
永远不会更改对象的实际类型并保留对象标识(请参阅分区I)。因此,性能杀手isn在这种情况下不是
isinst
,而是其他isinst
。从Hans的回答中并不清楚,因为他只看了JITed代码。通常,C#编译器会在unbox.any
之后发出unbox.any
(但如果isinst T?
是引用类型,则在执行isinst T
时会省略它)。为什么这样做呢? T
永远不会具有明显的效果,即您可以返回isinst T?
。取而代之的是,所有这些说明确保您拥有一个可以拆箱到T?
的"boxed T"
。要获得实际的T?
,我们仍然需要将T?
拆箱到"boxed T"
,这就是为什么编译器在T?
之后发出unbox.any
的原因。如果您考虑一下,这是有道理的,因为isinst
的“框格式”仅是T?
,而使"boxed T"
和castclass
执行取消框操作将是不一致的。标准,就可以了:(ECMA 335 Partition III,4.33):
isinst
当应用于值类型的盒装形式时,指令提取obj(类型为
unbox.any
)中包含的值。 (等效于unbox.any
,后跟O
。)当应用于引用类型时,unbox
指令与ldobj
typeTok具有相同的作用。(ECMA 335 Partition III,4.32):
unbox.any
通常,
castclass
只是计算装箱对象内部已经存在的值类型的地址。将可空值类型装箱时,这种方法是不可能的。由于在框操作期间unbox
的值会转换为带框的unbox
,因此实现通常必须在堆上制造新的Nullable<T>
并计算到新分配的对象的地址。评论
我认为最后引用的句子可能有错别字;不应“ ...在堆...上”是“在执行堆栈上?”好像取消装箱回到一些新的GC堆实例中,原来的问题换成了几乎相同的新问题。
– Glenn Slayden
19 Mar 2 '19 at 5:38
#4 楼
有趣的是,我通过dynamic
传递了有关操作员支持的反馈,该反馈对于Nullable<T>
的响应速度较慢(类似于此早期测试)-我怀疑原因非常相似。另一个有趣的事情是,即使JIT为非空结构都发现(并删除了)Nullable<T>
,但它却使null
陷入困境: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;
}
}
评论
尤瑟那真是一个痛苦的区别。真是的
–乔恩·斯基特(Jon Skeet)
09-10-17在21:32
如果所有这些都没有其他好处,那就让我在原始代码和代码中都包含警告:)
–乔恩·斯基特(Jon Skeet)
09-10-18在11:52
我知道这是一个古老的问题,但是您能否解释一下“对于不可为空的结构,JIT发现(并删除)null”是什么意思?您是说它在运行时将null替换为默认值或其他值吗?
–贾斯汀·摩根(Justin Morgan)
2011年4月21日在21:23
@Justin-泛型方法可以在运行时使用任何数量的泛型参数(T等)排列。堆栈等的要求取决于args(局部堆栈的堆栈空间等),因此对于涉及值类型的任何唯一置换,您都会获得一个JIT。但是,引用的大小都相同,因此共享一个JIT。在执行按值类型的JIT时,它可以检查一些明显的情况,并尝试消除由于无法实现空值之类的不可达代码。请注意,这并不完美。另外,我也忽略了上述的AOT。
– Marc Gravell♦
2011年4月21日在21:32
无限制的可空测试仍然要慢2.5个数量级,但是当您不使用count变量时,会进行一些优化。添加Console.Write(count.ToString()+“”);在watch.Stop()之后;在这两种情况下,其他测试的速度都降低了一个数量级,但不受限制的可空测试不会更改。请注意,在测试传递null的情况时,也会发生变化,以确认原始代码并未真正对其他测试进行null检查和递增。 Linqpad
–马克·赫德
2014年5月20日6:01
#5 楼
为了使此答案保持最新,值得一提的是,此页面上的大部分讨论现在都在使用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
}
我发现,新语法的一种常见用法是在编写实现
struct
(大多数情况下)的.NET值类型(即C#中的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
<有关进一步的测试,以证实我对新C#7语法的性能超过先前可用选项的评论,请参见此处(特别是示例) 'D')。
#6 楼
这是上述FindSumWithAsAndHas的结果:这是FindSumWithCast的结果:
发现:
使用
as
,它将首先测试对象是否是Int32的实例;在引擎盖下使用的是isinst Int32
(类似于手写代码:if(o is int))。并使用as
,也可以无条件地将对象拆箱。调用属性(它仍然是一个函数)是一个真正的性能杀手,IL_0027 使用强制转换,首先测试对象是否为
int
。在引擎盖下使用if (o is int)
。如果它是int的实例,则可以放心地将值IL_002D 简单地拆箱,这是使用
isinst Int32
方法的伪代码:int? x;
(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)
if (x.HasValue)
sum += x.Value;
这是使用强制转换方法的伪代码:实际上,拆箱,强制转换和拆箱使用相同的语法,下次我将使用正确的术语进行学步)方法确实更快,只需要在对象绝对是
as
时拆开值即可。使用(int)a[i]
方法不能说同一件事。#7 楼
进一步进行分析:首先,“先浇铸”方法要比“浇铸”方法更快。 303 vs 3524
第二。.value比强制转换稍慢。 3524 vs 3272
第三,.HasValue比使用手动has稍慢(即使用is)。 3524 vs 3282
第四,在模拟方法和真实方法之间进行一个苹果对苹果的比较(即同时分配模拟的HasValue和转换模拟的值),我们可以看到模拟方法仍然比真实方法更快如。 395 vs 3524
最后,基于第一个和第四个结论,as
实现有问题[^ _ ^
#8 楼
我没有时间尝试,但是您可能想要:/>您每次都创建一个新对象,虽然不能完全解释问题,但可能会有所帮助。
评论
不,我跑过了,速度慢了一点。
–汉克·霍尔特曼
09-10-17在20:03
在我的经验中,在不同位置声明变量只会对捕获到的变量产生重大影响(此时会影响实际的语义)。请注意,尽管它肯定是在创建int的新实例,但并未在堆上创建新的对象?使用unbox.any在堆栈上。我怀疑这就是问题所在-我的猜测是,手工制作的IL可能在这里击败了这两种选择...虽然JIT也有可能经过优化以识别is / cast情况,并且只检查一次。
–乔恩·斯基特(Jon Skeet)
09-10-17在20:08
我一直在考虑演员表的优化,因为它已经存在了很长时间。
–詹姆斯·布莱克(James Black)
09-10-17在20:17
is / cast是优化的一个简单目标,这是一个令人讨厌的惯用语。
– Anton Tykhyy
09-10-17在20:26
创建方法的堆栈框架时,将在堆栈上分配局部变量,因此在方法中声明变量的位置完全没有区别。 (除非它当然处于关闭状态,但是这里不是这种情况。)
–古法
09-10-17在21:30
#9 楼
我尝试了确切的类型检查构造体typeof(int) == item.GetType()
,其执行速度与item is int
版本一样快,并且始终返回数字(强调:即使您将Nullable<int>
写入数组,您也需要使用typeof(int)
)。您还需要在此处进行其他null != item
检查。然而
typeof(int?) == item.GetType()
保持快速状态(与item is int?
相反),但始终返回false。在我看来,typeof-construct是使用RuntimeTypeHandle进行准确类型检查的最快方法。由于这种情况下的确切类型与nullable不匹配,因此我猜想,
is/as
必须在此进行其他繁重的工作以确保它实际上是Nullable类型的实例。您的is Nullable<xxx> plus HasValue
买了什么?没有。您总是可以直接转到基础(值)类型(在这种情况下)。您要么得到值,要么是“否,不是您要查询的类型的实例”。即使将(int?)null
写入了数组,类型检查也将返回false。评论
有趣的是...使用“ as” + HasValue(不是加上HasValue,请注意)的想法是,它只执行一次类型检查,而不是两次。它一步完成“检查并取消装箱”。感觉应该更快一些……但事实并非如此。我不确定最后一句话是什么意思,但是没有盒装整数吗? -如果您将int装箱?值,其结果将以盒装int或null引用结束。
–乔恩·斯基特(Jon Skeet)
2010年6月19日在10:17
#10 楼
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(公司开发机器)进行配置调试。
以下操作是在我的机器上使用VS2010在Core 2 Duo上完成的。
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
评论
您不感兴趣使用的是哪个框架版本?我的上网本(使用.NET 4RC)上的结果更加引人注目-使用As的版本比您的结果差得多。也许他们已经对.NET 4 RTM进行了改进?我仍然认为这可能会更快...
–乔恩·斯基特(Jon Skeet)
2010年4月21日在6:06
@Michael:您正在运行未优化的版本,还是在调试器中运行?
–乔恩·斯基特(Jon Skeet)
2010年6月19日,9:10
@Jon:未优化的构建,在调试器下
–迈克尔·布恩(Michael Buen)
2010年6月19日,9:52
@Michael:对-我倾向于将调试器下的性能结果视为无关紧要的:)
–乔恩·斯基特(Jon Skeet)
2010年6月19日,9:53
@Jon:如果是在调试器下进行,则表示在VS内部;是的,以前的基准测试是在调试器下完成的。我再次在VS内部和外部进行基准测试,并分别编译为调试版本和发布版本。检查编辑
–迈克尔·布恩(Michael Buen)
2010年6月19日上午10:19
评论
为什么不看一下固定代码?甚至VS调试器也可以显示它。我很好奇,您也使用CLR 4.0进行了测试吗?
@安东:好点。会在某个时候做(尽管目前在VS中还没有:) @divo:是的,而且整个情况都更糟。但这就是beta版本,因此其中可能包含许多调试代码。
今天,我了解到您可以在可为空的类型上使用as。有趣,因为它不能在其他值类型上使用。其实更令人惊讶。
@Lepp对于不使用值类型非常有意义。考虑一下,尝试转换为类型,如果失败,则返回null。您不能将值类型设置为null