.NET 方法内联和循环






4.80/5 (9投票s)
本文深入探讨了 .NET 方法内联主题,并阐明了 CLR 更可能内联方法的一个特定情况 - 循环。
引言
正如您可能知道的那样,函数(方法)内联是一种优化技术,几乎所有现代语言编译器都会执行。本质上,方法内联是指编译器决定用函数的函数体替换您的函数调用。编译器这样做是为了节省实际进行函数调用的开销,这涉及到将每个参数推送到堆栈上、函数序言、函数后记等。.NET 也不例外。但请记住,负责决定是否内联方法的不是 C# 或 VB.NET 编译器,而是 JIT 编译器。因此,让我们看看什么会影响它的决定。
是否内联
根据 Sasha's Goldstein 的文章 http://blogs.microsoft.co.il/sasha/2012/01/20/aggressive-inlining-in-the-clr-45-jit/,JIT 不会内联
- 用
MethodImplOptions.NoInlining
标记的方法 - IL 超过 32 字节的方法
- 虚方法
- 将大值类型作为参数的方法
MarshalByRef
类上的方法- 具有复杂流程图的方法
- 满足其他更特殊特征的方法
但事实证明,同一个方法可以在一个上下文中内联,而在另一个上下文中不内联。让我们考虑一下传统的 JIT-x86 并创建一个简单的方法
public static class Utils
{
public static bool TryParse(char c, out int val)
{
if (c < '0' || c > '9')
{
val = 0;
return false;
}
val = (c - '0');
return true;
}
}
此方法仅检查字符是否为数字,并将转换后的字符作为输出参数返回。如果我们尝试调用此方法
var key = System.Console.ReadKey();
int val = 0;
if (Utils.TryParse(key.KeyChar, out val))
{
Console.WriteLine("Parsed");
}
并调查 JIT 编译的汇编代码,我们将看到以下代码片段(您可以使用 Visual Studio 或 WinDBG
深入研究 JIT 编译的机器码)。
0000001e xor edx,edx
00000020 mov dword ptr [ebp-10h],edx
00000023 movzx ecx,word ptr [ebp-0Ch]
00000027 lea edx,[ebp-10h]
0000002a call dword ptr ds:[00944D4Ch]
00000030 test eax,eax
00000032 je 0000003F
我们在 0000002a
地址处看到 call 指令。也就是说,方法 TryParse
没有被内联。这很可能是由于存在输出参数。
对于循环
现在,让我们修改方法的调用代码,以便从 for
循环内部调用 TryParse
方法。
var key = System.Console.ReadKey();
int val = 0;
for (int i = 0; i < 1; i++)
{
if (Utils.TryParse(key.KeyChar, out val))
{
Console.WriteLine("Parsed");
}
}
这是从上面的 C# 代码片段生成的汇编代码
00000024 xor esi,esi
00000026 movzx eax,word ptr [ebp-10h]
0000002a cmp eax,30h
0000002d jl 00000034
0000002f cmp eax,39h
00000032 jle 0000003D
00000034 xor edx,edx
00000036 mov dword ptr [ebp-14h],edx
00000039 xor eax,eax
0000003b jmp 00000048
0000003d add eax,0FFFFFFD0h
00000040 mov dword ptr [ebp-14h],eax
00000043 mov eax,1
00000048 test eax,eax
0000004a je 00000057
很明显,在这种情况下,该方法被内联了。我高亮显示了对应于下面 if
语句的比较和跳转指令
if (c < '0' || c > '9')
结论是,当方法在 for
循环内部调用时,JIT 更可能将其内联。但是其他类型的循环呢?
While 循环
让我们看一下 while
循环的例子
var key = System.Console.ReadKey();
int val = 0;
int i = 0;
while (i < 1)
{
i++;
if (Utils.TryParse(key.KeyChar, out val))
{
Console.WriteLine("Parsed");
}
}
while
循环的行为与 for
循环相同,即 JIT 在这种情况下内联 TryParse
方法。无限循环
但并非所有循环的行为都一样,因为涉及内联。让我们看一下功能上等效的无限 for 循环,该循环在第一次迭代后中断
var key = System.Console.ReadKey();
int val = 0;
for (;;)
{
if (Utils.TryParse(key.KeyChar, out val))
{
Console.WriteLine("Parsed");
}
break;
}
神奇的是,在这种情况下,尽管这个循环从逻辑上讲与之前的循环完全相同,但 JIT 决定不内联 TryParse
方法。 那么 foreach
循环呢?
foreach 循环
List<int> l = new List<int> { 0 };
int val = 0;
foreach (var i in l)
{
var key = System.Console.ReadKey();
if (Utils.TryParse(key.KeyChar, out val))
{
Console.WriteLine("Parsed");
}
}
您可以看到,JIT 产生了一个非常复杂的汇编代码片段
s00000039 lea edi,[ebp-4Ch]
0000003c xorps xmm0,xmm0
0000003f movq mmword ptr [edi],xmm0
00000043 movq mmword ptr [edi+8],xmm0
00000048 mov dword ptr [ebp-4Ch],esi
0000004b mov dword ptr [ebp-48h],edx
0000004e mov eax,dword ptr [esi+10h]
00000051 mov dword ptr [ebp-44h],eax
00000054 mov dword ptr [ebp-40h],edx
00000057 lea edi,[ebp-3Ch]
0000005a lea esi,[ebp-4Ch]
0000005d movs dword ptr es:[edi],dword ptr [esi]
0000005e movs dword ptr es:[edi],dword ptr [esi]
0000005f movs dword ptr es:[edi],dword ptr [esi]
00000060 movs dword ptr es:[edi],dword ptr [esi]
00000061 lea ecx,[ebp-3Ch]
00000064 call 70C2F368
00000069 test eax,eax
0000006b je 000000B4
0000006d lea ecx,[ebp-2Ch]
00000070 xor edx,edx
00000072 call 713461AC
00000077 movzx eax,word ptr [ebp-2Ch]
0000007b cmp eax,30h
0000007e jl 00000085
00000080 cmp eax,39h
00000083 jle 0000008E
但是很容易找到对应于 TryParse
方法中已经熟悉的 if
语句的 cmp
和 jump
(jl
和 jle
)指令。 也就是说,foreach
循环增加了 JIT 内联其中调用的方法的可能性。
ForEach 方法
有一个 Lst<T>
类的 ForEach
方法,它在功能上等同于 foreach
循环。 但是,如果我们将 foreach
替换为 ForEach
方法
int val;
List<int> l = new List<int> { 0 };
l.ForEach((i) =>
{
var key = System.Console.ReadKey();
if (Utils.TryParse(key.KeyChar, out val))
{
Console.WriteLine("Parsed");
}
});
将不会发生内联。
结论
尽管从循环内部调用方法增加了 JIT 内联方法的可能性,但并非所有类型的循环和类似循环的结构的行为都相同。 我们只介绍了 x32 版本的 JIT,您可能会问 x64 JIT 的行为是否与 x32 版本不同。 好问题。 我自己也问了同样的问题。 答案是 x64 JIT 的行为完全相同。 下表总结了我们在上面看到的内容。
方法调用 | x32 JIT | x64 JIT |
For 循环 |
内联 | 内联 |
While 循环 |
内联 | 内联 |
无限 循环 |
未内联 | 未内联 |
Foreach 循环 |
内联 | 内联 |
ForEach 方法 |
未内联 | 未内联 |
新的 RyuJIT
可能会有稍微不同的行为。如果您感兴趣并分享您的发现,您可以自己进行检查。
历史
- 版本 1 - 2016 年 1 月
- 版本 2 - 2016 年 1 月。也涵盖了 x64 JIT 版本