【问题标题】:JIT Optimization: Why Is It Slow and How Can I Improve It?JIT 优化:为什么它很慢以及如何改进它?
【发布时间】:2018-05-01 14:44:12
【问题描述】:

我一直在研究使用 JIT 内联的方法,并找到了 this article by Scott Hanselman。我把他的代码更进一步,看起来虽然当代码在 Release 模式下运行时只有几个调用堆栈,但实际上似乎在运行时似乎那些额外的帧仍然存在于已编译的代码中(即使它们确实存在不报告)。

首先,如果您想进入并运行它,我已将代码放在这里: https://github.com/Mike-EEE/StackOverflow.Performance

我已经在 .NET 4.7.1、.NET Core 2.0 甚至最近宣布的新 .NET Core 2.1 Preview 上进行了尝试。所有功能都具有相同的结果。

我所做的是创建一个发出消息的简单命令,然后创建一个后续的多修饰命令,将这个简单命令多次包装。在发布的代码中,这种修饰完成了 10 次,从而产生了一个具有 10 级的嵌套命令(如果算上原始简单命令,则为 11 级)。

测试中使用的这两个命令都使用空委托来发出消息,因为在性能测试期间使用Console.WriteLine 会变得相当丑陋。

在运行测试之前,我确实创建了一个修饰命令,它使用与测试代码相同的代码,但不是空委托,而是使用 Console.WriteLine 来验证当前执行环境中的堆栈跟踪。

在 Debug 中,此堆栈跟踪如下所示:

   at StackOverflow.Performance.EmitMessage.Emit(String message)
   at StackOverflow.Performance.EmitMessage.MethodC(String message)
   at StackOverflow.Performance.EmitMessage.MethodB(String message)
   at StackOverflow.Performance.EmitMessage.MethodA(String message)
   at StackOverflow.Performance.EmitMessage.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.DecoratedCommand.Execute(String message)
   at StackOverflow.Performance.Program.Main()

在 Release 中,它看起来像这样:

   at StackOverflow.Performance.EmitMessage.Emit(String message)
   at StackOverflow.Performance.Program.Main()

到目前为止,一切看起来都很棒,完全符合我的预期。但是,然后我通过BenchmarkDotNet 执行这两个命令,以查看性能设置中的结果。这些结果似乎确实表明修饰命令的调用链已完全执行,即使发出的堆栈跟踪表明不存在这样的调用链:

// * Summary *

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-4820K CPU 3.70GHz (Haswell), 1 CPU, 8 logical and 8 physical cores
.NET Core SDK=2.1.300-preview2-008533
  [Host]     : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT


    Method |      Mean |     Error |    StdDev |
---------- |----------:|----------:|----------:|
    Direct |  3.581 ns | 0.0759 ns | 0.0710 ns |
 Decorated | 44.646 ns | 0.7701 ns | 0.7203 ns |

因此,这里似乎执行了超过 2 个帧,这导致我在 StackOverflow 上发布了这个问题。我确实有几个问题:

  1. 我的代码有什么根本不准确的地方吗?这将是难以置信令人尴尬,但我想首先清除明显的。 :)
  2. 如果我的代码和结果确实准确,那么:这是一个已知问题吗?和/或这是否按设计执行?
  3. 我的假设是这是正在使用的尾调用优化。难道这里也发生了方法内联?我想我的基本问题是:究竟在优化什么?
  4. 最重要的是:是否有办法确保并实现我正在寻找的优化结果?传递给根代表的任何类型的魔法在这里都是有价值的。似乎根委托已正确解析,只是未正确调用

为了完整起见,以下是运行此示例的所有代码:

public class Program
{
    static void Main()
    {
        // Writes out the stack trace from a decorated command:
        Decorate.Get(new EmitMessage(Console.WriteLine))
                .Execute(null);

        BenchmarkRunner.Run<Program>();
        Console.ReadKey();
    }

    readonly ICommand _direct, _decorated;
    readonly string _message;

    public Program() : this(new EmitMessage()) {}

    public Program(ICommand direct) : this(direct, Decorate.Get(direct), "Hello World!") {}

    public Program(ICommand direct, ICommand decorated, string message)
    {
        _direct    = direct;
        _decorated = decorated;
        _message = message;
    }

    [Benchmark]
    public void Direct()
    {
        _direct.Execute(_message);
    }

    [Benchmark]
    public void Decorated()
    {
        _decorated.Execute(_message);
    }
}

static class Decorate
{
    public static ICommand Get(ICommand parameter)
        => Enumerable.Range(0, 10)
                     .Aggregate(parameter, (command, _) => new DecoratedCommand(command));
}

sealed class DecoratedCommand : ICommand
{
    readonly ICommand _command;

    public DecoratedCommand(ICommand command) => _command = command;

    public void Execute(string message)
    {
        _command.Execute(message);
    }
}

sealed class EmitMessage : ICommand
{
    readonly Action<string> _emit;

    public EmitMessage() : this(_ => {}) {}

    public EmitMessage(Action<string> emit) => _emit = emit;

    public void Execute(string message)
    {
        MethodA(message);
    }

    void MethodA(string message)
    {
        MethodB(message);
    }

    void MethodB(string message)
    {
        MethodC(message);
    }

    void MethodC(string message)
    {
        Emit(message);
    }

    void Emit(string message)
    {
        _emit(message ?? new StackTrace().ToString());
    }
}

public interface ICommand
{
    void Execute(string message);
}

提前感谢您提供的任何见解/帮助!

【问题讨论】:

  • JIT 编译代码需要时间,所以第一次通过可能会很慢。你应该在不止一个循环中运行你的代码并平均时间。
  • 这很好,@RonBeyer。据我了解,BenchmarkDotNet 应该在其测试运行中自动为您处理。尽管如此,我确实尝试在命令上运行一些预执行,然后再进行测试并得到相同的结果。

标签: c# .net performance jit


【解决方案1】:

无耻地抄袭here的Stephen Toub的作品

我刚刚查看了 DecoratedCommand 的反汇编,使用经过检查的 coreclr 构建并使用 setCOMPlus_JitDisasm=Execute 运行,请参阅 documentation。事实上,它正在使用尾调用:

;方法DecoratedCommand:Execute(ref):this

的组装清单

;使用 AVX 为 X64 CPU 发出 BLENDED_CODE

;优化代码

;基于rsp的框架

;完全可中断

;最终局部变量赋值

; V00 this [V00,T00] ( 3, 3 ) ref -> rcx this class-hnd

; V01 arg1 [V01,T01] ( 3, 3 ) ref -> rdx class-hnd

;# V02 OutArgs [V02] (1, 1) lclBlk (0) [rsp+0x00]

; Lcl 帧大小 = 0

G_M223_IG01:

G_M223_IG02:

488B4908 mov rcx, gword ptr [rcx+8]

49BB48007733FD7F0000 mov r11, 0x7FFD33770048

488B05934FE5FF mov rax, qword ptr [(reloc)]

3909 cmp dword ptr [rcx], ecx

G_M223_IG03:

48FFE0 rex.jmp rax

【讨论】:

  • FWIW @Dwayne 反对不是我的,但欢迎来到 StackOverflow。 ;) 正如您在该线程中看到的那样,我仍然不清楚优化是如何发生的,并且进一步不清楚为什么该方法没有内联,因为它似乎能够解决它。此外,我正在寻找描述如何减少开销的答案。
  • 哈哈,谢谢。我并不太担心,只是觉得我偶然发现了一些有价值的东西。你看过this吗?不过,我不确定您如何使聚合函数“语义相同”。
  • 是的,这很有趣。我有点开始觉得这个问题会导致一个 GitHub 问题/请求,我自己。除非有什么超级魔法成功逃脱了我的研究。不过,我认为 Toub 先生会提到一些事情。或者我的问题迷失了。那好吧。 FWIW我会赞成你的回答,因为它是我所见过的最接近的近似值,而且是正确的——老实说! ——参考。此外,反对票的人应该发布他们反对票的理由。 :P
  • FWIW,预测的 GitHub 问题/请求已经出现,哈哈。如果您想继续关注,可以在这里找到:github.com/dotnet/coreclr/issues/1784#issuecomment-392554011
  • 恭喜@Dwayne,你的无耻得到了回报。除了上述问题之外,GitHub 存储库中还提供了另一个问题:github.com/dotnet/coreclr/issues/6737 因此,似乎这些是那里的团队捕获的已知限制,并希望在未来的版本中得到解决,也许通过分层吉亭。除了您(或者更确切地说,Stephen 的 :))的回答/努力之外,这为我提供了足够的背景和信息,让我有信心暂时理解这个问题,并希望未来能够解决剩下的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-26
  • 2021-05-06
  • 1970-01-01
  • 2010-10-17
相关资源
最近更新 更多