在编程语言中,类型转换是一个常见的操作,它允许程序员在不同的数据类型之间进行转换。虽然许多程序员可能认为类型转换是一个简单的操作,但实际上,不同类型的转换对程序的性能有不同的影响。本文将分析类型转换的常见情况、编译器的行为以及MSIL(Microsoft Intermediate Language)生成的代码,以帮助程序员更好地理解类型转换对程序性能的影响,并提供一些优化建议。
原始类型是指那些可以直接由虚拟机指令处理的非组合类型,例如int、long、float等。这些类型没有内部结构,并且总是按值传递,除非程序员明确指定其他行为(使用out和ref修饰符)。下面是一个关于使用和转换原始类型的简单示例:
int z = 10;
double r = 3.4;
uint n = 20;
r = z; // 隐式从int转换为double
z = (int)r; // 显式从double转换为int
n = (uint)z; // 显式从int转换为uint
这个示例在原始类型集合中执行了一些转换,在某些情况下将转换任务留给编译器,在其他情况下明确标记转换。接下来,将深入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指令,其功能是将栈顶的值转换为操作码中指定的类型(r4、i4等)。从现在起,知道“无辜”的显式和隐式转换在原始类型之间生成指令,这些指令可以通过一致的类型使用来避免。同样的转换也适用于64位数据类型,如double、long和ulong。
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操作码,如果可能的话,将引用转换为括号中指定的类型(如果不可以,则抛出InvalidCastException异常)。在第二种情况下,as运算符被翻译为IsInst操作码,它的工作速度更快,因为它只检查引用类型,而不执行任何类型的转换(也不抛出任何异常)。
现在让做相反的事情!现在是时候爬到类层次结构的顶部,看看这些类型的转换有多慢(或多快)。以下示例创建了一个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"参数)。无论如何,似乎微软C#编译器没有内联这些方法,所以在栈中安排参数和返回地址可能会减慢代码执行速度。