具有绝对返回路径的方法是如何内联的

本文关键字:方法 何内联 返回路径 | 更新日期: 2023-09-27 18:29:35

我主要使用C#进行开发,但我认为这个问题可能也适用于其他语言
此外,这里似乎有很多代码,但问题很简单

据我所知,内联是编译器(在C#虚拟机的情况下)通过在调用方法的每个地方插入方法体来替换方法调用。

假设我有以下程序:

static Main()
{        
    int number = 7;
    bool a;
    a = IsEven(number);
    Console.WriteLine(a);
}

方法主体IsEven:

bool IsEven(int n) 
{
    if (n % 2 == 0) // Two conditional return paths
        return true;
    else
        return false;
}

我可以理解内联方法后代码的样子:

static Main()
{
    int number = 7;
    bool a;
    if (number % 2 == 0)
        a = true;
    else
        a = false;
    Console.WriteLine(a); // Will print true if 'number' is even, otherwise false
}

一个明显简单而正确的程序。

但是,如果我稍微调整一下IsEven的主体,使其包含一个绝对返回路径。。。

bool IsEven(int n)
{
    if (n % 2 == 0)
        return true;
    return false; // <- Absolute return path!
}

在某些情况下,我个人更喜欢这种风格。一些折射工具甚至可能建议我把第一个版本改成这样——但当我试图想象这个方法内联时会是什么样子时,我被难住了
如果我们内联方法的第二个版本:

static Main()
{
    int number = 7;
    bool a;
    if (number % 2 == 0)
        a = true;
    a = false;
    Console.WriteLine(a); // Will always print false!
}

要问的问题:
编译器/虚拟机如何处理具有绝对返回路径的方法的内联
像这样的事情似乎不太可能真正阻止方法内联,所以我想知道如何处理这些事情。也许内联的过程并没有这么简单?也许某个版本更有可能被VM内联?

编辑:分析这两个方法(以及第一个方法的手动内联)没有显示出性能上的差异,所以我只能假设两个方法都是内联的,并且以相同或相似的方式工作(至少在我的VM上)
此外,这些方法非常简单,似乎几乎可以互换,但具有绝对返回路径的复杂方法可能更难更改为没有绝对返回路径。

具有绝对返回路径的方法是如何内联的

很难解释JITter在内联时会做什么-它不会改变C#代码来进行内联-它将(总是?)处理生成的字节(编译版本)-并且在生成汇编代码(实际的机器代码字节)时使用的"工具"比C#(或IL)中使用的"更细粒度"。

也就是说,您可以通过考虑break关键字来了解它是如何工作的,用C#术语来说。。

考虑这样一种可能性,即每个不平凡的内联函数都包含在while (true)循环(或do while(false)循环)中,并且每个源返回都被转换为localVar = result; break;语句集。然后你会得到这样的东西:

static Main() 
{ 
    int number = 7; 
    bool a; 
    while (true)
    {
       if (number % 2 == 0) 
       {
          a = true; 
          break;
        }
        a = false; 
        break;
    }
    Console.WriteLine(a); // Will always print the right thing! Yey!
}

类似地,在生成汇编时,你会看到生成了很多jmp——这些在道德上相当于break语句,但它们更灵活(可以把它们想象成匿名goto之类的)。

所以你可以看到,抖动(以及任何编译为本机的编译器)手头有很多工具可以用来做"正确的事情"。

return语句指示:

  • 结果值
  • 局部变量的销毁(这适用于C++,而不是C#)在C#中,finally块和using块中的Dispose调用将运行
  • 跳出函数

所有这些事情在内联之后仍然会发生。内联后,跳转将是本地的,而不是跨函数的,但它仍然存在。

内嵌不是像C/C++宏那样的文本替换。

内联和文本替换之间的其他不同之处在于对同名变量的处理。

cpu执行的机器代码是一种非常简单的语言。它没有return语句的概念,子程序有一个入口点和一个出口点。所以你的IsEven()方法是这样的:

bool IsEven(int n)
{
    if (n % 2 == 0)
        return true;
    return false;
}

需要通过抖动重写为类似的东西(无效C#):

void IsEvent(int n) 
{
    if (n % 2 == 0) {
       $retval = true;
       goto exit;
    }
    $retval = false;
exit:
}  // $retval becomes the function return value

$retval变量在这里看起来可能是假的。它不是,它是x86内核上的EAX寄存器。您现在将看到,此代码易于内联,可以直接移植到Main()的主体中。$retval变量可以通过简单的逻辑替换等同于a变量。