【发布时间】: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 上发布了这个问题。我确实有几个问题:
- 我的代码有什么根本不准确的地方吗?这将是难以置信令人尴尬,但我想首先清除明显的。 :)
- 如果我的代码和结果确实准确,那么:这是一个已知问题吗?和/或这是否按设计执行?
- 我的假设是这是正在使用的尾调用优化。难道这里也发生了方法内联?我想我的基本问题是:究竟在优化什么?
- 最重要的是:是否有办法确保并实现我正在寻找的优化结果?传递给根代表的任何类型的魔法在这里都是有价值的。似乎根委托已正确解析,只是未正确调用。
为了完整起见,以下是运行此示例的所有代码:
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