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

.NET 方法内联和循环

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (9投票s)

2016年1月17日

CPOL

3分钟阅读

viewsIcon

32556

本文深入探讨了 .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");
   }
}
您可以尝试自己调查 JIT 编译的汇编代码,并确保 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 语句的 cmpjumpjljle)指令。 也就是说,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 版本
© . All rights reserved.