C# 中类型转换对执行性能的影响






4.71/5 (30投票s)
2004年8月22日
5分钟阅读

267053

2
本文分析了 C# 中最常见的类型转换情况以及编译器在这些情况下的行为。
引言
显式和隐式类型转换是几乎所有命令式编程语言中常见的编程主题。大多数 C、C++ 或 Pascal 程序员关心代码的效率和速度;但那些使用托管编程环境(如 Java、Visual Basic 或 C#)的程序员则将所有优化任务都委托给编译器和运行时环境。
在许多情况下,这可能是一个好方法,但托管语言也越来越多地被用于高性能应用程序,在这些应用程序中,了解语言、编译器和运行时环境可以提高程序的质量和速度。
本文分析了最常见的类型转换情况以及编译器在这些情况下的行为。我们将研究生成的 MSIL 代码,但由于实现和供应商的依赖性,我们将不研究特定于机器的指令序列。
转换基本类型
基本类型是那些可以由(虚拟)机器指令直接处理的非复合类型,即 `int`、`long`、`float` 等。这些类型没有内部结构,并且程序员没有明确指定其他行为(使用 `out` 和 `ref` 修饰符)的情况下,它们始终按值传递。让我们看一个使用和转换基本类型的简单示例。
int z = 10;
double r = 3.4;
uint n = 20;
r = z; // Implicit conversion from int to double (1)
z = (int)r; // Explicit conversion from double to int (2)
n = (uint)z; // Explicit conversion from int to uint (3)
此示例执行了基本类型集合中的一些转换,在某些情况下将转换任务留给编译器,而在其他情况下则显式标记转换。
好的,现在是时候深入研究生成的 MSIL 代码,并检查类型转换对我们代码的影响了。
.locals init ([0] int32 z,
[1] float32 r,
[2] unsigned int32 n)
IL_0000: ldc.i4.s 10
IL_0002: stloc.0
IL_0003: ldc.r4 (9A 99 59 40)
IL_0008: stloc.1
IL_0009: ldc.i4.s 20
IL_000b: stloc.2 //(1)
IL_000c: ldloc.0
IL_000d: conv.r4
IL_000e: stloc.1
IL_000f: ldloc.1 //(2)
IL_0010: conv.i4
IL_0011: stloc.0
IL_0012: ldloc.0 //(3)
IL_0013: stloc.2
IL_0014: ret
正如我们所见,代码中有几个 `Conv.XY` 指令,其作用是将堆栈顶部的 a 值转换为操作码中指定的类型(r4、i4 等)。从现在开始,我们知道“无害”的显式和隐式基本类型转换会生成指令,而一致的类型使用可以避免这些指令。在 64 位数据类型(如 `double`、`long` 和 `ulong`)中也应用了相同的转换。
请注意,由于涉及的类型(`int` 和 `uint`)的性质,最后一个类型转换不需要显式的“`Conv`”操作码;这些类型具有非常相似的存储结构(大端字节序,有符号类型中有一个符号位),并且转换符号问题必须由程序员控制。
基本类型的一个特例是 `bool`(内部作为 `int` 处理),它与数值类型之间的转换(反之亦然)在 C# 中是不允许的,因此我们不会对其进行研究。
对象引用的向下转型
C# 提供了两种转换对象引用的方法(请注意,除非是上一节讨论的类型,否则所有类型都是引用类型)。
object myClass = new MyClass();
((MyClass)myClass).DoSome(); //(1)
(myClass as MyClass).DoSome(); //(2)
前面的示例是向下转型(从类层次结构的顶部到底部进行转换)的一个好例子。用于执行转换的方法似乎相同,但生成的 MSIL 序列略有不同。
.locals init ([0] object myClass)
IL_0000: newobj instance void Sample.MyClass::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0 //(1)
IL_0007: castclass Sample.MyClass
IL_000c: callvirt instance void Sample.MyClass::DoSome()
IL_0011: ldloc.0 //(2)
IL_0012: isinst Sample.MyClass
IL_0017: callvirt instance void Sample.MyClass::DoSome()
IL_001c: ret
在第一行代码中,编译器发出一个“`Castclass`”操作码,该操作码在可能的情况下将 a 引用转换为括号中指定的类型(否则会引发 `InvalidCastException` 异常)。
在第二种情况下,`as` 运算符被转换为“`IsInst`”操作码,该操作码运行速度更快,因为它只检查引用类型,而不执行任何类型的转换(也不会引发任何异常)。
在性能方面,我们更倾向于第二种选择,因为“`IsInst`”可以大大加快代码执行速度,避免了类型转换和异常抛出。以下是使用“`as`”运算符获得的 a 速度提升示例。
另一方面,括号内的转换提供了更好的错误控制,避免了使用“`as`”运算符进行无效类型转换时获得的 a 空引用错误。
对象引用的向上转型
让我们做相反的操作!现在是时候爬升类层次结构,看看这些类型的转换有多慢(或快)。以下示例创建一个 `MyDerivedClass` 类型的对象,并将其引用存储在 `MyClass` 类型变量中。
MyDerivedClass myDerivedClass = new MyDerivedClass();
MyClass myClass = myDerivedClass;
生成的代码是:
.locals init ([0] class Sample.MyDerivedClass myDerivedClass,
[1] class Sample.MyClass myClass)
IL_0000: newobj instance void Sample.MyDerivedClass::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: stloc.1
IL_0008: ret
正如我们所见,没有转换操作码,只有引用加载和存储。这对我们的效率很有好处……正如预期的那样,向上转型类型检查是在编译时完成的,运行时成本与同一类型变量之间的简单赋值一样低。
转换运算符
C# 语言包含一项很棒的功能,允许定义隐式和显式转换运算符。这些转换方法的效率取决于转换方法的实现。无论如何,这些函数始终是静态的,并且只有一个参数,因此过程调用开销很小(不需要传递“`this`”参数)。无论如何,似乎 Microsoft C# 编译器不会内联这些方法,因此在堆栈上排列参数和返回地址可能会降低代码执行速度。
整合所有内容
以下是一些基于前面几节结果的优化程序的通用技巧。
- 数值类型转换通常很昂贵,将它们移出循环和递归函数,并在可能的情况下使用相同的数值类型。
- 向下转型是一项伟大的发明,但所涉及的类型检查对执行性能有很大的影响,请在循环和递归函数之外检查对象类型,并在其中使用“`as`”运算符。
- 向上转型很便宜!在您需要它的任何地方都使用它。
- 构建轻量级转换运算符以加快自定义转换速度。
使用的工具
所有测试和反汇编均使用 .NET Framework SDK 中包含的工具进行。`ILDasm` 可以让你了解程序的许多性能缺陷,所以多玩玩它。