MSIL 的可能增强






4.70/5 (4投票s)
2002年10月25日
7分钟阅读

125342
可以通过添加一些新指令来改进 Microsoft Intermediate Language (MSIL)
摘要
Microsoft 的 .NET Framework 是一个旨在简化分布式应用程序开发的新平台。 .NET 的底层是 CLR,即所谓的通用语言运行时。 CLR 提供了一个控制 .NET 代码的环境,例如内存管理、线程执行、代码检查、安全服务和垃圾回收。 CLR 还实现了一个称为 CTS(通用类型子系统)的严格基础设施,用于检查代码和类型,即在一个语言中创建的类型可以在另一种语言中安全地使用和引用。托管执行环境解决了与传统本机代码执行相关的某些问题。 CLR 自动处理对象及其引用的排列,并在不再需要时释放它们,从而避免无效引用和内存泄漏。 CLR 不依赖于本机 x86 指令集。相反,Microsoft 创建了一种中间语言 (IL),该语言由所有支持 .NET 平台的编译器生成。 Microsoft 中间语言 (MSIL) 是一种与处理器无关的指令集,包含执行控制、直接内存访问、异常处理、逻辑和算术运算的指令,以及加载、存储和初始化对象的指令。 MSIL 代码不能直接执行;它必须由 JIT(即时)编译器转换为特定于处理器的代码。任何支持的架构都必须提供其特定的 JIT 编译器。
当我为 Delta Forth .NET 编译器 (http://www.dataman.ro/dforth) 编写代码生成器时,我发现 IL 在一定程度上可以得到改进。本文介绍了一种针对 Microsoft 中间语言的建议增强方案。
Microsoft 中间语言
MSIL 与我们所知的传统汇编语言不同。尽管具有低级指令,但该语言也处理高级概念,如异常处理和内存管理。 IL 还有一个虚拟参数堆栈,可以容纳不同类型的对象,如字符串、整数、浮点数、指针等。堆栈规则更严格,这意味着在执行命令时,堆栈必须包含所需的准确参数数量。相反,x86 汇编语言允许堆栈上有任意数量的参数,甚至提供了在离开过程时清理堆栈的指令(参见ret n)。
大多数指令都涉及堆栈操作
- LOAD 指令(ldarg, ldloc, ldnull 等),将值推送到堆栈
- STORE 指令(starg, stloc, stind 等),将值从堆栈弹出
- DUP 指令 – 复制堆栈顶部的元素
- POP 指令 – 移除堆栈顶部的元素
有关 IL 指令集的更多信息,请参阅 [2]。
尽管 IL 指令集足以实现其目标,但它似乎会导致生成效率低下的代码。在开发支持 .NET 平台的 Forth 编译器过程中,我发现生成的代码在很多时候比应有的要长,这主要是由于缺少一些简单的指令。为了让读者理解下面的代码序列,有必要了解 Forth 编程语言的一些堆栈处理原语
Forth 原语 |
堆栈转换 |
解释 |
DUP |
( A – A A ) |
复制堆栈顶部的元素 |
OVER |
( A B – A B A ) |
复制堆栈顶部的第二个元素 |
SWAP |
( A B – B A ) |
交换堆栈顶部两个元素 |
ROT |
( A B C – B C A ) |
旋转堆栈顶部三个元素 |
1+ |
(A – A+1) |
将堆栈顶部的元素加 1 |
2+ |
(A – A+2) |
将堆栈顶部的元素加 2 |
堆栈转换列显示了原语执行前后的堆栈状态。两个状态由连字符分隔。
对于习惯于传统命令式语言的程序员来说,在 Forth 中解决数学表达式非常复杂,但没有什么特别之处,除了使用逆波兰表示法 (RPN)。有关更多阅读,我推荐 [3]。
让我们以表达式为例
(A + B) * (A – B)
其后缀等价式为
A B + A B - *
解决该表达式的 Forth 原语序列(假设 A 和 B 的值已按此顺序推入堆栈)是
OVER (A B – A B A)
OVER (A B A – A B A B)
+ (A B A B – A B C),其中 C = A + B
ROT (A B C – B C A)
ROT (B C A – C A B)
- (C A B – C D),其中 D = C – B
* (C D – E),其中 E = C * D
理解上面的代码将帮助您理解接下来的 IL 代码片段。让我们继续看看…
幕后
看到用支持 .NET 的语言编写的程序的编译生成的 IL 代码很有意思。我选择的语言是 C#,它似乎非常有前景。要重复下面的实验,您需要 Visual Studio .NET 和 .NET Framework IL Disassembler。
我们将用于测试的类非常简单,即使是初学者也能轻松理解
class UnderTheHood
{
private static int[] x = new int [] {1, 2, 3};
private static int i = 0, j = 0, t = 0;
static void Main(string[] args)
{
// Here we will insert code snippets
}
}
我们来看第一个例子
x[i + 1] = i + 2;
编译器在 Main 函数中生成的代码是
IL_0000: ldsfld int32[] CallTest.UnderTheHood::x
IL_0005: ldsfld int32 CallTest.UnderTheHood::i
IL_000a: ldc.i4.1
IL_000b: add
IL_000c: ldsfld int32 CallTest.UnderTheHood::i
IL_0011: ldc.i4.2
IL_0012: add
IL_0013: stelem.i4
IL_0014: ret
第一行和第二行将静态变量 x 和 I 的值推入堆栈。 i 的值会增加(通过推送 1 并执行加法)。增量操作(这次是加 2)在 IL_000c 和 IL_0012 行重复。最终,结果在 IL_0013 行存储在其目标位置。
乍一看,我们看到代码序列分别计算了两个索引 i+1 和 i+2。如果我们回顾上一段中的 **1+** Forth 原语,我们会发现使用它将带来更高的效率,特别是由于大多数现代处理器都有 INC 和 DEC 指令。从 IL 到本机代码的翻译将是即时的。修改后的序列可能如下所示
IL_0000: ldsfld int32[] CallTest.UnderTheHood::x
IL_0005: ldsfld int32 CallTest.UnderTheHood::i
IL_000a: ldc.i4.1
IL_000b: add
IL_000c: dup
IL_000d: inc.1 // Non-existent mnemonic, see text
IL_000e: stelem.i4
IL_000f: ret
我选择了助记符 inc.1,表示堆栈顶部的的值加 1。类似地,我们可以为其他操作编写 inc.2、dec.1、dec.2。收益是 7 字节。
我们再次进行实验,这次使用以下序列
x[i + 1] += 1;这种情况下的 IL 序列是
.locals ( [0] int32[] CS$00000002$00000000,
[1] int32 CS$00000002$00000001)
IL_0000: ldsfld int32[] CallTest.UnderTheHood::x
IL_0005: dup
IL_0006: stloc.0
IL_0007: ldsfld int32 CallTest.UnderTheHood::i
IL_000c: ldc.i4.1
IL_000d: add
IL_000e: dup
IL_000f: stloc.1
IL_0010: ldloc.0
IL_0011: ldloc.1
IL_0012: ldelem.i4
IL_0013: ldc.i4.1
IL_0014: add
IL_0015: stelem.i4
IL_0016: ret
我们看到编译器自动生成了两个匿名的局部变量来保存 x(IL_0006 行)和 i+1(IL_000f 行)的值。可以使用 OVER Forth 原语大大优化此序列,该原语复制堆栈上第二高的元素。重写的 IL 代码如下所示
IL_0000: ldsfld int32[] CallTest.UnderTheHood::x
IL_0005: dup
IL_0006: stloc.0
IL_0007: ldsfld int32 CallTest.UnderTheHood::i
IL_000c: inc.1 // Non-existent mnemonic, see text
IL_000d: over // Non-existent mnemonic, see text
IL_000e: over
IL_000f: ldelem.i4
IL_0010: ldc.i4.1
IL_0011: add
IL_0012: stelem.i4
IL_0013: ret
收益是 6 字节。
一个相当常见的序列是交换存储在两个变量中的元素。 C# 程序如下所示
t = i; i = j; j = t;
等效的 IL 代码是
IL_0000: ldsfld int32 CallTest.UnderTheHood::i
IL_0005: stsfld int32 CallTest.UnderTheHood::t
IL_000a: ldsfld int32 CallTest.UnderTheHood::j
IL_000f: stsfld int32 CallTest.UnderTheHood::i
IL_0014: ldsfld int32 CallTest.UnderTheHood::t
IL_0019: stsfld int32 CallTest.UnderTheHood::j
IL_001e: ret
请注意,IL 序列紧密跟随 C# 程序。如果高级语言本机为程序员提供 SWAP 指令,那将很有趣,因此前面的代码片段可以重写如下
IL_0000: ldsfld int32 CallTest.UnderTheHood::i
IL_0005: ldsfld int32 CallTest.UnderTheHood::j
IL_000a: swap // Non-existent mnemonic, see text
IL_000f: stsfld int32 CallTest.UnderTheHood::j
IL_0014: stsfld int32 CallTest.UnderTheHood::i
IL_0019: ret
这里节省了 9 字节,还不包括不声明中间临时变量 (t) 所节省的空间。通过完全替换 SWAP 操作,代码可以进一步优化
IL_0000: ldsfld int32 CallTest.UnderTheHood::i
IL_0005: ldsfld int32 CallTest.UnderTheHood::j
IL_000a: stsfld int32 CallTest.UnderTheHood::i
IL_000f: stsfld int32 CallTest.UnderTheHood::j
IL_0014: ret
结论
虽然乍一看空间节省不大,但实际上情况略有不同。这类代码序列在日常应用程序中相对常见。如果我们考虑一个中等规模的应用程序可能包含大约 200 个此类可优化构造,并且平均节省量每个构造约为 8 字节,那么我们可以节省 1.6KB 的代码空间,这对于嵌入式系统来说是不可忽略的。
空间节省并不是支持使用新的 IL 指令的唯一论据。 JIT 生成的原生代码可以通过使用这些指令获得更有效的转换,因为可以使用一些专用的处理器指令。
作者简介
[1] Microsoft Developer Network, http://msdn.microsoft.com
[2] Common Language Infrastructure, Partition III (CIL Instruction Set)
[3] Forth Expressions, Valer BOCAN, NET Report, January 2002 (Romanian)
[4] Delta Forth .NET Development System, (C)1997-2002 Valer BOCAN, http://www.dataman.ro/dforth