65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (30投票s)

2004年8月22日

5分钟阅读

viewsIcon

267053

downloadIcon

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` 可以让你了解程序的许多性能缺陷,所以多玩玩它。

© . All rights reserved.