性能的惊喜的",为"与空类型
-
21-09-2019 - |
题
我只是在修订的第4章的C#在深入处理与空类型,并且我加入一段关于使用"如"经营者,它允许你写:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
我认为这是非常整洁,它可以提高业绩C#1的相当,使用的是"是"之后的铸毕竟,这样一来我们只需要要求为动态型检查一次,然后简单的价值,检查。
这似乎不是这种情况,但是。我已经包括一个样品测试的应用程序下,它的款项基本上所有的整数在内的一个目列-但阵列中含有大量的空引用和串引用,以及装箱的整数。基准的措施代码你会有使用C#1,代码使用"如"经营者,只是踢一个皇宫的解决方案。我惊讶的是,C#1的代码是20倍的速度在这种情况下,甚至皇宫代码(我们预期的要慢,给予的迭代参与)比"作为"的代码。
是的.净执行情况 isinst
对于空类型只是真的很缓慢?这是额外的 unbox.any
导致该问题?是否有另一种解释这个吗?在目前这感觉就像我必须要包括一个警告,反对使用这一敏感的性能的情况下...
结果:
演员:10000000:121
为:10000000:2211
皇宫: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编译器可以生成对于第一种情况是更有效的。一个规则确实有助于有一个对象只能是拆箱到具有相同类型的装箱值的变量。其允许JIT编译器生成非常有效的代码,没有价值转换必须考虑。
在就是操作者测试很容易,只要检查对象不为空,并且是预期的类型,但需要一些机器代码指令。演员阵容也很容易,JIT编译器知道在对象的值位的位置,并直接使用它们。没有复制或转换时,所有的机器代码是内嵌的需要,但十几个指令。这需要在.NET 1.0真正有效回来时,拳击是常见的。
铸造为int?需要更多的工作。盒装整数的值表示不与Nullable<int>
的存储器布局兼容。 A转换是必需的,该代码是棘手由于可能盒装枚举类型。 JIT编译器生成名为JIT_Unbox_Nullable把工作做了CLR辅助函数的调用。这是任何值类型的通用功能,大量的代码那里检查类型。和值将被复制。很难估计,因为这些代码被锁定内部的Mscorwks.dll的成本,但是数百机器码指令是可能的。
LINQ的OfType()扩展方法也使用就是运算符和铸造。然而,这是一个强制转换为泛型类型。 JIT编译器生成到一个辅助功能,JIT_Unbox()可以执行一个铸造成任意值类型的呼叫。我没有一个很好的解释为什么它很慢,投给Nullable<int>
,因为更少的工作应该是必要的。我在这里怀疑ngen.exe可能会引起麻烦。
其他提示
在我看来,该isinst
只是对空类型很慢。在方法FindSumWithCast
我改
if (o is int)
到
if (o is int?)
这也显著减慢执行。在IL我的唯一differenc可以看到的是
isinst [mscorlib]System.Int32
被改变成
isinst valuetype [mscorlib]System.Nullable`1<int32>
这一最初开始作为一个意见Hans顺便是良好的答案,但它得到了太长时间,所以我想补充几位在这里:
第一,C# as
操作员将发出一个 isinst
IL指示(所以不会 is
操作者)。(另一个有趣的指令 castclass
,emited当你做一个直接的演员和编译器知道运行时检查不可ommited.)
这里是什么 isinst
不(通信机制详解335分区III,4.6):
格式: isinst typeTok
typeTok 是的元数据标记(a
typeref
,typedef
或typespec
),表示希望类。如果 typeTok 是一个非空值的类型或一个通用的参数类型它解释为"装箱" typeTok.
如果 typeTok 是一个可空的类型
Nullable<T>
, 它解释为"装箱"T
最重要的是:
如果实际类型(未检验者跟踪类型) obj 是 验证程序可转让的-到 类型typeTok然后
isinst
成功和 obj (作为 结果,)是回不变,同时核查其轨道类型 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
执行拆箱将是不一致的。
备份汉斯的发现一些信息 标准, ,在这里它是:
(通信机制详解335分区三4.33): unbox.any
当应用到盒装形式的价值类,
unbox.any
指令中提取价值内所包含的obj(类型O
).(它是相当于unbox
随后通过ldobj
.) 当适用于基准类型,unbox.any
指令具有相同的效果castclass
typeTok.
(通信机制详解335分区三4.32): unbox
通常,
unbox
简单地计算的地址值的类型,已经存在的内部装箱的对象。这种做法是不可能的,当拆箱nullable值的类型。因Nullable<T>
值都转换为盒装的Ts
在箱操作,执行往往必须制造一个新的Nullable<T>
在堆和计算地址新分配的对象。
有趣的是,我通过反馈有关操作者的支持通过 dynamic
作为一个数量级速度较慢 Nullable<T>
(类似于 这种早期的测试)-我怀疑为非常类似的原因。
一定要爱 Nullable<T>
.另一个有趣的是,即使JIT点(并删除) null
对于非nullable结构,它borks它 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;
}
}
调查结果:
使用
as
, 它测试第一,如果对象是一个实例Int32;引擎盖下的就是使用isinst Int32
(类似于手写代码:如果(o int)).和使用as
, 它也无条件地拆箱的对象。它是一个真正的表现-凶手叫酒店(它仍然是一个功能的引擎盖下的),IL_0027使用铸造、测试第一,如果对象是
int
if (o is int)
;发动机罩下,这是使用isinst Int32
.如果它是一个实例中的诠释,那么你就可以安全地拆箱的价值,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
办法。
分析进一步:
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
我们可以推断出这些数字吗?
- 首先,是的-然后-铸的办法是显着快于 作为 办法。303vs3524
- 第二,.值略低于铸造。3524vs3272
- 第三,.HasValue略慢于使用手册(即使用 是).3524vs3282
- 第四,做一个苹果-对-苹果的比较(即两分配的模拟HasValue和模拟转换值发生的一起)之间 模拟 和 真的 方法,我们可以看到 模拟 是仍然明显快于 真的.395vs3524
- 最后,根据第一个和第四个结论,有些东西错了 作为 执行^_^
为了保持这个答案的日期,这是值得一提的是,大多数讨论这一页是现在没有实际意义现在 C#7.1 和 .净4.7 这支持一个苗条的语法,也会产生最好的IL代码。
运算是原来的例子...
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
在 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码为新的语法是确1byte较小,这主要是赢得大通过使零电话(对两个)和避免 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')。
我没有时间去尝试它,但你可能想有:
foreach (object o in values)
{
int? x = o as int?;
作为
int? x;
foreach (object o in values)
{
x = o as int?;
您每次创建一个新对象时,这将不能完全解释的问题,但也可以作出贡献。
我试图确切类型检查构建体
typeof(int) == item.GetType()
,其执行以最快的速度item is int
版本,并始终返回数(强调:即使你写了一个Nullable<int>
到数组,你需要使用typeof(int)
)。您还需要在这里的附加null != item
检查。
然而
typeof(int?) == item.GetType()
停留快速(与item is int?
),但始终返回false。
typeof运算的构建是在我眼睛的最快方法的确切的类型检查,因为它使用的RuntimeTypeHandle。因为在这种情况下,确切类型不可空匹配,我的猜测是,is/as
要在这里做更多的heavylifting于确保它实际上是一个可空类型的实例。
和诚实:那么你的is Nullable<xxx> plus HasValue
买吗?没有。您可以随时直接到底层(值)类型(在这种情况下)。你要么获得的价值或“不,不是你问的类型的实例”。即使你写(int?)null
到数组中,类型检查将返回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] 强>
注意:上试验VS内,配置调试完成,使用VS2009,使用的Core i7(公司开发机器)。
以下使用酷睿2我的机器上进行的,使用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