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

MSIL 的可能增强

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (4投票s)

2002年10月25日

7分钟阅读

viewsIcon

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

© . All rights reserved.