考虑以下代码:
static void FillUsingAsNullable()
{
int?[] arr = new int?[1 << 24];
var sw = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < arr.Length; ++i)
arr[i] = GetObject() as int?;
Console.WriteLine("{0:N0}", sw.ElapsedTicks);
}
static void FillUsingOwnCode()
{
int?[] arr = new int?[1 << 24];
var sw = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < arr.Length; ++i)
{
object temporary = GetObject();
arr[i] = temporary is int ? (int?)temporary : null;
}
Console.WriteLine("{0:N0}", sw.ElapsedTicks);
}
static object GetObject()
{
//Uncomment only one:
//return new object();
//return 42;
//return null;
}
据我所见,方法FillUsingAsNullable
和FillUsingOwnCode
应该是等价的。
但看起来"自己的代码"版本显然更快。
有2
选项可用于编译"x86"或"x64",有2
选项可用于编辑"Debug"或"Release(optimization)",还有3
选项可用于GetObject
方法中返回的内容。据我所见,在所有这些2*2*3 == 12
的情况下,"自己的代码"版本明显快于"可以为null"的版本。
问题是:as
和Nullable<>
是否不必要地慢,或者我在这里错过了什么(很可能)?
相关线程:具有"as"和可为null类型的性能惊喜。
生成的IL不同,但并非根本不同。如果JIT很好,但事实并非如此,这也不是什么新闻,那么它可以编译成完全相同的x86代码。
我用VS2010 Release AnyCPU编译了这个。
as
版本:
L_0015: call object ConsoleApplication3.Program::GetObject()
L_001a: stloc.3
L_001b: ldloc.0
L_001c: ldloc.2
L_001d: ldelema [mscorlib]System.Nullable`1<int32>
L_0022: ldloc.3
L_0023: isinst [mscorlib]System.Nullable`1<int32>
L_0028: unbox.any [mscorlib]System.Nullable`1<int32>
L_002d: stobj [mscorlib]System.Nullable`1<int32>
?:
版本:
L_0015: call object ConsoleApplication3.Program::GetObject()
L_001a: stloc.3
L_001b: ldloc.0
L_001c: ldloc.2
L_001d: ldelema [mscorlib]System.Nullable`1<int32>
L_0022: ldloc.3
L_0023: isinst int32
L_0028: brtrue.s L_0036 //**branch here**
L_002a: ldloca.s nullable
L_002c: initobj [mscorlib]System.Nullable`1<int32>
L_0032: ldloc.s nullable
L_0034: br.s L_003c
L_0036: ldloc.3
L_0037: unbox.any [mscorlib]System.Nullable`1<int32>
L_003c: stobj [mscorlib]System.Nullable`1<int32>
操作码的描述在MSDN上。理解这个IL并不困难,任何人都可以做到。不过,对于缺乏经验的人来说,这有点耗时。
主要的区别是源代码中有分支的版本在生成的IL中也有分支。它只是稍微不那么优雅。如果愿意的话,C#编译器本可以对此进行优化,但团队的策略是让JIT担心优化。如果JIT得到了必要的投资,它会很好地工作。
您可以通过查看JIT发出的x86来进一步分析这一点。你会发现一个明显的区别,但这将是一个不起眼的发现。我不会花时间去做那件事。
我修改了as
版本,以使用临时版本进行公平比较:
var temporary = GetObject();
arr[i] = temporary as int?;
是的,as运算符是为了方便而非性能,因此速度较慢。
有关更多信息,您可以参考以下答案:
";作为";关键字内部工作?
我认为您的测试结果主要是测量误差。
这是我的程序:
static void Main()
{
FillUsingAsNullable();
FillUsingOwnCode();
FillUsingAsNullable();
FillUsingOwnCode();
Console.ReadLine();
}
以下是我在Release中使用return 42
:在调试器外部运行的内容
2,540,125
1,975,131
2,407,204
2,246,339
请注意,运行之间存在相当大的差异。您可能需要在系列中再运行几次,才能获得良好的性能指标。
以下是当我们将GetObject()
更改为返回(int?)42
时发生的情况。
7,829,214
7,941,861
8,001,102
7,124,096
同样,使用相同的配置:
8,243,258
7,114,879
7,932,285
7,268,167
如果你真的想收集有意义的数据,我建议以不同的顺序重复两次测试,重复几次,并查看结果的平均值和标准差。
我怀疑这些方法中最大的时间损失是内存缓存无效,因此给定测试的结果可能取决于数组最终分配给每个测试的确切位置,以及GC在频繁的装箱分配过程中何时启动。