几天前,在写关于溢出的这个问题的答案时,我对 C# 编译器感到有点惊讶,它没有做我期望它做的事情。请看以下代码片段:
第一:
object[] array = new object[1];
for (int i = 0; i < 100000; i++)
{
ICollection<object> col = (ICollection<object>)array;
col.Contains(null);
}
第二:
object[] array = new object[1];
for (int i = 0; i < 100000; i++)
{
ICollection<object> col = array;
col.Contains(null);
}
两个片段之间代码的唯一区别是转换为
ICollection<object>
。因为 object[]
显式实现了 ICollection<object>
接口,所以我希望这两个代码片段编译为相同的 IL,因此是相同的。然而,当对它们进行性能测试时,我注意到后者的速度大约是前者的 6 倍。
比较两个片段中的 IL 后,我注意到这两种方法是相同的,除了第一个片段中的
castclass
IL 指令。
对此感到惊讶,我现在想知道为什么 C# 编译器在这里不“智能”。事情从来没有看起来那么简单,那么为什么 C# 编译器在这里有点幼稚呢?
我的猜测是您在优化器中发现了一个小错误。其中有数组的各种特殊情况代码。感谢您引起我的注意。
这是一个粗略的猜测,但我认为这是关于数组与其通用 IEnumerable 的关系。
在 .NET Framework 2.0 版中, 数组类实现了 System.Collections.Generic.IList, System.Collections.Generic.ICollection, 和 System.Collections.Generic.IEnumerable 通用接口。这 提供给数组的实现 在运行时,因此不是 对文档构建可见 工具。结果,通用的 界面中没有出现 数组的声明语法 类,并且没有参考 接口成员的主题是 只能通过将数组转换为 通用接口类型(显式 接口实现)。关键 当你施放时要注意的事情 这些接口之一的数组是 添加、插入或 删除元素抛出 不支持异常。
请参阅 MSDN 文章。
尚不清楚这是否与 .NET 2.0+ 有关,但在这种特殊情况下,如果表达式仅在运行时有效,那么编译器无法优化表达式的原因就很明显了。
这看起来只不过是编译器错过了抑制强制转换的机会。 如果你这样写就可以了:
ICollection<object> col = array as ICollection<object>;
这表明它变得过于保守,因为强制转换可能会引发异常。 但是,当您转换为非泛型 ICollection 时,它确实有效。 我的结论是他们只是忽略了这一点。
这里存在一个更大的优化问题,JIT 编译器不应用循环不变提升优化。 它应该像这样重写代码:
object[] array = new object[1];
ICollection<object> col = (ICollection<object>)array;
for (int i = 0; i < 100000; i++)
{
col.Contains(null);
}
例如,这是 C/C++ 代码生成器中的标准优化。 尽管如此,JIT 优化器无法在发现此类可能的优化所需的分析上消耗大量周期。 令人高兴的是,优化的托管代码仍然是相当可调试的。 C# 程序员仍然可以编写高性能代码。