VB vs. C# MSIL 代码生成:结果是否相同?






4.60/5 (31投票s)
2005 年 2 月 7 日
11分钟阅读

302157

322
讨论 VB 和 C# MSIL 代码之间的一些差异。
引言
本文的目的是通过实证证据,一劳永逸地证明 VB 和 C# 编译器生成的 MSIL 代码并不相同,尤其是在处理非 void 方法和字符串值比较时。为了证明这一点,我们将对比两个完全相同的程序集,一个用 VB 编写,另一个用 C# 编写,它们各自编译器生成的 MSIL 代码,并可能解释这些差异存在的原因。之后,将简要讨论这种差异的重要性,并附带一些可能引发 VB vs. C# 语言大战的带有偏见的评论。
测试程序集
测试中使用的两个程序集在所有方面都完全相同。但是,这里我只提供每个程序集暴露的唯一类型的代码,以及代表这些完全相同类型暴露的仅有的两个方法的生成的 MSIL 代码。为了验证“其他因素”也相等,例如优化设置,您可以随意下载代码。此外,两个程序集都在 Windows XP SP2 机器上使用 VS.NET 2003(.NET 1.1 SP1)编写和编译。而且,MSIL 代码是使用 ildasm.exe(版本 1.1.4322.573)获得的。
VB 类型 ClassLibrary1.Class1
Public Class Class1
Public Sub New()
End Sub
Public Function foo1(ByVal test As Boolean) As Integer
Dim i As Integer
If (test) Then
i = 1
Else
i = 2
End If
Return i
End Function
Public Sub foo2(ByVal test As Boolean)
Dim s As String = Nothing
If s = String.Empty Then
End If
End Sub
End Class
C# 类型 ClassLibrary1.Class1
public class Class1
{
public Class1()
{
}
public int foo1(bool test)
{
int i;
if(test)
i=1;
else
i=2;
return i;
}
public void foo2(bool test)
{
string s = null;
if(s == string.Empty){};
}
}
现在我们有了两个完全相同的类型,前者是用 VB 编写的,后者是用 C# 编写的。首先,让我们比较一下每个编译器为成员 foo1
生成的 MSIL 代码。
VB MSIL (foo1)
.method public instance int32 foo1(bool test) cil managed
{
// Code size 11 (0xb)
.maxstack 1
.locals init (int32 V_0,
int32 V_1)
IL_0000: ldarg.1
IL_0001: brfalse.s IL_0007
IL_0003: ldc.i4.1
IL_0004: stloc.1
IL_0005: br.s IL_0009
IL_0007: ldc.i4.2
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: ret
} // end of method Class1::foo1
C# MSIL (foo1)
.method public hidebysig instance int32 foo1(bool test) cil managed
{
// Code size 11 (0xb)
.maxstack 1
.locals init (int32 V_0)
IL_0000: ldarg.1
IL_0001: brfalse.s IL_0007
IL_0003: ldc.i4.1
IL_0004: stloc.0
IL_0005: br.s IL_0009
IL_0007: ldc.i4.2
IL_0008: stloc.0
IL_0009: ldloc.0
IL_000a: ret
} // end of method Class1::foo1
乍一看,两个编译器生成的 MSIL 代码看起来完全相同。首先,每个版本都有相同的 MSIL 代码行数。其次,两个版本都有相同的 .maxstack
指令,这意味着每个方法期望使用相同数量的堆栈槽。然而,如果您查看第三行 MSIL 代码,即 .locals init
指令,它用于声明变量并为它们分配初始值,两个版本之间存在一个惊人的差异。
VB: .locals init (int32 V_0,int32 V_1)
C#: .locals init (int32 V_0)
为什么尽管方法 foo1
只使用了(并且仅使用)一个局部变量(i
),VB MSIL 代码却声明并初始化了一个额外的变量 V_0
,而这个变量从未被使用?另一方面,C# MSIL 代码声明并初始化了一个变量,这与前面提到的显而易见的原因完全一致。
现在的问题是,为什么会有这种差异,无论它多么重要或不重要。我不是专家,尤其是在处理 MSIL 方面,但我几乎 100% 确定,未使用变量的存在仅仅是因为,与 C# 不同,VB 允许函数的代码路径**不**返回一个值,当发生这种情况时,未使用的 V_0
变量就会被使用。换句话说,在 VB 中,您可以有一个具有特定返回类型的函数,但如果您不显式返回该类型的值,编译器将为您返回该类型的默认值,因此,出现了神秘的 V_0
变量。为了证明我的观点,让我们注释掉 foo1
方法 VB 版本中最后一行,也就是显式返回函数值的行。
Public Function foo1(ByVal test As Boolean) As Integer
Dim i As Integer
If (test) Then
i = 1
Else
i = 2
End If
'Return i
End Function
做出上述更改并编译后,生成的 MSIL 代码如下所示:
.method public instance int32 foo1(bool test) cil managed
{
// Code size 11 (0xb)
.maxstack 1
.locals init (int32 V_0,
int32 V_1)
IL_0000: ldarg.1
IL_0001: brfalse.s IL_0007
IL_0003: ldc.i4.1
IL_0004: stloc.1
IL_0005: br.s IL_0009
IL_0007: ldc.i4.2
IL_0008: stloc.1
IL_0009: ldloc.0
IL_000a: ret
} // end of method Class1::foo1
VB foo1
MSIL 代码的两个版本之间的唯一区别在于倒数第二行。第一个版本显式返回一个值,将变量 V_1
推送到堆栈(通过 ldloc.1
),因为 V_1
代表变量 i
,而 i
持有显式返回的值。与之对比的是第二个版本,它不显式返回一个值,因此,编译器通过指令 ldloc.0
将变量 V_0
(它始终持有函数返回类型的默认值)推送到堆栈。
所以这里的故事的寓意是,对于用 VB 编写的任何非 void 方法,编译器将生成 MSIL 代码,该代码**总是**声明并初始化一个与其包含函数类型相同的额外局部变量,这个变量可能被使用,也可能不被使用,分别取决于该函数是否显式返回值。我个人认为,尽管额外的变量在性能和内存消耗方面的影响微不足道,但 VB 编译器应该足够智能,仅当成员的代码路径不返回值时才包含额外的变量。如果它返回,那么声明和初始化额外的变量毫无疑问是毫无意义的。
现在,让我们继续查看 VB 和 C# 编译器为方法 foo2
生成的 MSIL 代码。
VB MSIL (foo2)
.method public instance void foo2(bool test) cil managed
{
// Code size 18 (0x12)
.maxstack 3
.locals init (string V_0)
IL_0000: ldnull
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldsfld string [mscorlib]System.String::Empty
IL_0008: ldc.i4.0
IL_0009: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.
CompilerServices.StringType::StrCmp(string,
string,
bool)
IL_000e: ldc.i4.0
IL_000f: bne.un.s IL_0011
IL_0011: ret
} // end of method Class1::foo2
C# MSIL (foo2)
.method public hidebysig instance void foo2(bool test) cil managed
{
// Code size 15 (0xf)
.maxstack 2
.locals init (string V_0)
IL_0000: ldnull
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldsfld string [mscorlib]System.String::Empty
IL_0008: call bool [mscorlib]System.String::op_Equality(string,
string)
IL_000d: pop
IL_000e: ret
} // end of method Class1::foo2
好吧,通过快速浏览为方法 foo2
生成的两个版本的 MSIL 代码,很明显**确实**存在差异,而且比 foo1
的情况下的差异要明显得多。
首先,比较每个版本的代码大小注释,很明显 VB 版本(//Code size 18 (0x12)
)比 C# 版本(//Code size 15 (0xf)
)涉及更多的 MSIL 代码。
其次,VB 版本期望并使用三个堆栈槽(.maxstack 3
),而 C# 版本期望并只使用两个堆栈槽(.maxstack 2
)。
从那里开始,所有内容都保持不变,直到我们到达 IL_0008
行,此时两个版本都急剧分歧。
C# 版本首先将对 [mscorlib]System.String::op_Equality
的调用生成的 Boolean
结果推送到堆栈,这是 C# 使用 `==` 运算符比较字符串值时使用的函数。然后,在将执行返回给调用者之前,它会立即从堆栈中移除(pop
)这个相同的 Boolean
值。虽然最后的 pop
看起来是多余的,但实际上别无选择,因为在将控制权返回给调用者之前,方法的求值堆栈必须为空,除非要返回值,就像非 void 方法一样。
另一方面,VB 版本首先将值 0(ldc.i4.0
)推送到堆栈,该值随后将被弹出作为对 Microsoft.VisualBasic.CompilerServices.StringType::StrCmp
的调用的参数。这是 VB 使用 `=` 运算符比较字符串值时使用的函数,其 Integer
结果被推送到堆栈。完成此操作后,再次使用指令 ldc.i4.0
将值 0 推送到堆栈,以便随后可以将其与 StrCmp
的 Integer
结果进行比较。然后,通过指令 bne.un.s IL_0011
,将最后推送到堆栈的两个值(即对 StrCmp
和 ldc.i4.0
的调用结果)弹出、比较,如果结果不相等,则执行转移到指令 IL_0011
,这似乎是多余的,因为指令 IL_0011
紧随其后,无论结果是否不相等,但最有可能用于弹出堆栈上剩余的最后两个值。
因此,再次出现的问题是,为什么会有这种差异,无论它多么重要或不重要。嗯,在这种情况下,答案是清晰而简单的。出于向后兼容的原因,VB 被迫使用 StrCmp
而不是 String::op_Equality
,其结果差异很大,其中最显著的是 StrCmp
认为空字符串和 Null
字符串是相等的,而 String::op_Equality
则不是,而且,这很可能是前一个被使用而不是后一个的主要原因。从纯粹的性能角度来看,String::op_Equality
优于 StrCmp
,尽管性能提升只会在进行大量字符串比较时体现出来,比如在紧凑循环中进行的那些。此外,还有一个简单的解决方法可以在必须最大化性能的情况下消除这个问题,那就是在比较字符串值时使用 String.Equals
或 op_Equality
而不是 `=` 运算符。例如,将 foo2
的 VB 版本更改为
Public Sub foo2(ByVal test As Boolean)
Dim s As String = Nothing
If String.op_Equality(s, String.Empty) Then
End If
End Sub
将生成以下 MSIL 代码:
.method public instance void foo2(bool test) cil managed
{
// Code size 16 (0x10)
.maxstack 2
.locals init (string V_0)
IL_0000: ldnull
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldsfld string [mscorlib]System.String::Empty
IL_0008: call bool [mscorlib]System.String::op_Equality(string,
string)
IL_000d: brfalse.s IL_000f
IL_000f: ret
} // end of method Class1::foo2
请注意,现在 MSIL 代码的两个版本是相同的,除了倒数第二个指令。C# 版本只是通过 `pop` 指令弹出堆栈上最后剩下的值,而 VB 版本弹出堆栈上的最后一个值,将其与零进行比较,如果相等,则将控制权转移到指令 `IL_000f`,所有这些都是通过 `brfalse.s L_000f` 指令完成的,在这种情况下,这似乎是多余的,因为原始的 `if` 语句块是空的,因此编译器应该足够智能地忽略它,尽管另一方面,编译器也足够智能地知道,在大多数(如果不是全部)情况下,`if` 语句块不会是空的,因此,可能没必要去检测它。
测试结果的重要性
读完本文后,您可能已经得出结论,我是一名 VB 程序员,试图证明一个观点。嗯,您只对了一半。我当然在试图证明一个观点,那就是,由于 VB 语言的内在特性、您的使用方式、与遗留代码的向后兼容性以及导致无操作的逻辑假设,VB 编译器生成的代码**几乎**与 C# 编译器一样高效,但**不如** C# 编译器高效。
(**注意**:旨在促进 VB vs. C# 语言大战的带有偏见的陈述即将开始,您可能想停止阅读。)
话虽如此,但我必须明确一点,自从我获得 MIS 专业的 BS 学位并找到第一份工作以来,我所有的命令式编程都使用 VB。它是一种出色的语言,自 4.0 版本以来,特别是自 7.0 版本(.NET)以来,它不像 C# 那样将程序员限制在单一的编程范式。关于本文重点关注的 MSIL 代码差异的重要性,除了那些鄙视(CG)VB 的程序员之外,谁在乎呢?当然,付钱给我们写代码的人根本不在乎本文中所有的陈述,我向您保证,如果我试图基于本文的内容向一位了解 .NET 的 CIO/CTO 证明 C# 比 VB “更好”,我可能会因为完全缺乏商业判断而被解雇。此外,去找 Jim(财务副总裁),给他看本文包含的 MSIL 代码,并试图证明他部门的所有 VB 应用程序都应该用 C# 重建,我保证您将成为本季度的笑柄,尽管我们这里的一些 C# 人员,尤其是其中一位,足够大胆尝试这样的事情。
实际上,我写这篇文章的唯一原因就是往 VB vs. C# 大战的火焰上浇油。我厌倦了听到所有关于所有 .NET 语言都相等的论调,因为事实并非如此,VB 无疑优于 C#。此外,我也同情那些支持 C# 比 VB “更好”的荒谬论点,这些论点很容易被否定。诸如“它太啰嗦”或“存在太多糟糕的 VB 代码”之类的说法根本站不住脚。如果您在这里或任何地方开始 C# vs. VB 的辩论,请确保给我链接,以便我能参与。
请不要认真对待我的胡言乱语。我尊重你们所有人。只是我对 C# 语言毫无敬意。它不过是另一个 RAD 工具,仅此而已,它很幸运地借鉴了各种其他语言(包括 VB)的经验(好的和坏的),而且,它不必担心现有的遗留代码。我的态度可能会在 MS Office 完全用 C# 重写的那一天改变。
最终想法
最后,我相当确信,我关于 C# 和 VB 编译器之间 MSIL 代码生成比较的大部分结论都是正确的。然而,正如我已经提到的,我不是专家,但如果您是专家,并且知道我不知道的事情,或者能够发现我的错误,请告诉我。