【问题标题】:Understanding automatic inlining: when can the compiler inline methods involving private variables & abstract methods?了解自动内联:编译器何时可以内联涉及私有变量和抽象方法的方法?
【发布时间】:2017-08-02 06:09:01
【问题描述】:

使用 C#,但我认为这个问题也与其他(大多数与 c 相关的)语言有关。 考虑一下...

private float radius = 0.0f; // Set somewhere else
public float GetDiameter() {
   return radius * 2.0f;
}

如果在其他类中调用,编译器会内联吗?我认为答案当然是,但这里有一些困惑:radius 是私有的。所以从手动编程的角度来看,我们不可能内联这个方法,因为 radius 是私有的。

那么编译器做了什么?我认为它无论如何都可以内联它,因为如果我没记错的话,“私人”“公共”等。修饰符只影响人类编写的代码,而汇编语言可以根据需要访问自己程序的任何部分?

好的,但是抽象呢? 考虑一下...

public abstract class Animal {
   abstract public bool CanFly();
}

public class Hawk : Animal {
...
   override public bool CanFly() {
      if (age < 1.0f) return false; // Baby hawks can't fly yet
      return true;
   }
}

public class Dog : Animal {
...
   override public bool CanFly() {
      return false;
   }
}

在非动物类中:

...
Animal a = GetNextAnimal();
if (a.CanFly()) {
...

这可以内联吗?我几乎可以肯定不会,因为编译器不知道正在使用哪种动物。但如果我这样做了……

...
Animal a = new Hawk();
if (a.CanFly()) {
...

这有什么不同吗?如果没有,这个肯定可以吗?:

...
Hawk a = new Hawk();
if (a.CanFly()) {
...

如果我不使用上面的 bool 方法,有什么改变吗:

float animalAge = a.GetAge();

一般来说,过多的抽象 getter 和 setter 会导致性能下降吗?如果这达到了重要的程度,那么最好的解决方案是什么?

【问题讨论】:

  • 我认为在 C# 世界(以及许多其他基于 VM 的语言)中,这变得更加复杂,因为有一个从 C# 到 IL 的编译器可以进行一些优化,包括内联并且有一个 VM JIT 编译器也进行了一些优化,包括内联,但 JIT 编译器也可能依赖于一些统计指标,例如注意到 GetNextAnimal 总是以某种方式生成 Hawk 并因此将 'Hawk' 的代码与一个小的 pre-检查/捕获此条件是否成立(如果不生成更多/不同的代码)
  • 观看 YouTube 上的 "When Does the JVM JIT & Deoptimize?" 视频 这是针对 Java 的,但 C# 类似。它展示了许多惊人的技巧,但当他描述“推测优化”时,一些与您的问题最相关的技巧会在 1:00 - 1:15 左右显示。还可以查看slideshare.net/dougqh/jvm-mechanics-when-does-the 上同一演示文稿的幻灯片 #89,其中显示了一些相关的 JVM 配置,例如 MaxInlineLevel 另见 Inline caching Wiki 上的文章

标签: performance compilation inline private abstract


【解决方案1】:

通常没有简单的方法可以预先预测方法是否会被内联。您必须实际编写一个程序并查看为其生成的机器代码。这在 C 程序中很容易做到,您可以要求编译器生成汇编代码列表(例如 /FA 用于 MSVC,-S 用于 GCC)。

由于即时编译代码的抖动,在 .NET 中更加复杂。从技术上讲,优化器的源代码可从 CoreCLR 项目获得,但很难弄清楚它的作用,很多非常坚不可摧的 C++ 代码。您必须利用 Visual Studio 中的“视觉”并使用调试器。

这需要一些准备才能确保您获得实际的优化代码,它通常会禁用优化器以简化调试。切换到发布配置并使用工具 > 选项 > 调试 > 常规 > 取消选中“抑制 JIT 优化”复选框。如果你想要最佳的浮点代码,那么你总是想要 64 位代码,所以使用 Project > Properties > Build 选项卡,取消勾选“Prefer 32-bit”。

并编写一个小测试程序来练习该方法。这可能很棘手,您可能很容易最终没有代码。在这种情况下很容易, Console.WriteLine() 是强制使用此方法的好方法,它无法被优化掉。所以:

class Program {
    static void Main(string[] args) {
        var obj = new Example();
        Console.WriteLine(obj.GetDiameter());
    }
}

class Example {
    private float radius = 0.0f;
    public float GetDiameter() {
        return radius * 2.0f;
    }
}

在 Main() 上设置断点并按 F5。然后使用 Debug > Windows > Disassembly 查看机器代码。在我的带有 Haswell 内核(支持 AVX)的机器上,我得到:

00007FFEB9D50480  sub         rsp,28h                   ; setup stack frame
00007FFEB9D50484  mov         rcx,7FFEB9C45A78h         ; rcx = typeof(Example)
00007FFEB9D5048E  call        00007FFF19362530          ; rax = new Example()
00007FFEB9D50493  vmovss      xmm0,dword ptr [rax+8]    ; xmm0 = Example.field
00007FFEB9D50499  vmulss      xmm0,xmm0,dword ptr [7FFEB9D504B0h]  ; xmm0 *= 2.0
00007FFEB9D504A2  call        00007FFF01647BB0          ; Console.WriteLine()
00007FFEB9D504A7  nop                                   ; alignment
00007FFEB9D504A8  add         rsp,28h                   ; tear down stack frame
00007FFEB9D504AC  ret 

我对代码进行了注释以帮助理解它,如果您以前从未看过它可能会很神秘。但毫无疑问,您可以看出该方法已内联。没有 CALL 指令,它被内联到两条指令(VMOVSS 和 VMULSS)。

正如你所料。可访问性在内联决策中没有任何作用,它是一个简单的代码提升技巧,不会改变程序的逻辑操作。它首先对 C# 编译器很重要,紧挨着内置在抖动中的验证器,但随后作为代码生成器和优化器的关注点消失了。

对抽象类做同样的事情。您会看到该方法没有被内联,需要一个间接的 CALL 指令。即使方法是完全空的。当某些语言编译器知道对象的类型但 C# 编译器不是其中之一时,它们可以将虚拟方法调用转换为非虚拟调用。抖动优化器也没有。编辑:recent work 已完成去虚拟化调用。

一个方法不会被内联还有其他原因,一个移动的目标很难记录。但粗略地说,具有太多 MSIL、try/catch/throw、循环、CAS 要求、一些退化的结构案例、MarshalByRefObject 基础的方法不会被内联。务必查看实际的机器代码以确定。

[MethodImpl(MethodImplOptions.AgressiveInlining)] 属性可以强制优化器重新考虑 MSIL 限制。 MethodImplOptions.Noinlining 有助于禁用内联,您可能希望这样做以获得更好的异常堆栈跟踪或减慢抖动,因为可能未部署程序集。

this post 中有关抖动优化器执行的优化的更多信息。

【讨论】:

    猜你喜欢
    • 2021-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-13
    • 2022-08-02
    • 2021-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多