我只是在修改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>,并计算新分配对象的地址。
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
有趣的是,对于Nullable<T>(类似于早期的测试),我通过dynamic传递了关于操作符支持的反馈——我怀疑是出于非常相似的原因。
爱上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;
}
}
这最初是对汉斯·帕桑的精彩回答的评论,但它太长了,所以我想在这里添加一些内容:
首先,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>,并计算新分配对象的地址。