【问题标题】:Can .NET JIT optimization inline this method.NET JIT 优化可以内联这个方法吗
【发布时间】:2018-06-04 16:33:06
【问题描述】:

我知道静态方法可以通过 .Net(和 Mono)中的 JIT 优化内联

我的问题是,访问自己的状态的实例方法也可以内联吗?

例如:

public class CaseSensitiveLiteralStringMatcher : IStringMatcher
{
    private readonly LiteralToken _token;

    public CaseSensitiveLiteralStringMatcher(LiteralToken token)
    {
        _token = token;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool IsMatch(char containsChar, int position)
    {
        return containsChar == _token.Value[position];       
    }

}

上面的方法调用是否会被内联,即使它不是静态的并且访问一些私有成员?

【问题讨论】:

    标签: c# .net optimization jit


    【解决方案1】:

    我在这里找到了一个很好的阅读:http://blogs.microsoft.co.il/sasha/2007/02/27/jit-optimizations-inlining-and-interface-method-dispatching-part-1-of-n/

    我的结论是,实例方法可以内联,但虚拟方法不能,因为调用的实际方法在运行时会发生变化,无法通过源代码的静态分析来建立。

    出于这个原因,我在我的问题中显示的方法可以被内联,如果它不是接口方法 - 因为这意味着它是虚拟的,因为它必须在运行时通过 vtable 查找进行调度。

    也就是说,有一些 JIT 优化技术可以针对“常见”情况优化虚拟方法内联,但是当内联方法在运行时与所需的方法调用不匹配时,这些技术会提供回退,这意味着某些代码路径可能比其他路径更受益于内联。

    【讨论】:

    【解决方案2】:

    好的。我有结果。答案似乎是 JIT 可以内联实现接口并访问或修改类成员的方法。

    我的结果是:

    • process1 运行 10^7 次:84 毫秒
    • process2 运行 10^7 次(通过接口):83 毫秒
    • 10^7 次内联循环运行,没有类或方法调用:83 毫秒

    即具有和不具有接口的相同性能。此外,在没有编译器激进的内联指令的情况下,性能保持不变。

    测试代码:

    class Program
    {
        internal interface IFastProcessor
        {
            void Process(int i);
        }
    
        internal sealed class FastProcessorImpl : IFastProcessor
        {
            private int number;
    
            public FastProcessorImpl(int number)
            {
                this.number = number;
            }
    
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void Process(int i)
            {
                number = ((number + i) / (number + i)) * number;
            }
        }
    
        internal sealed class FastProcessor
        {
            private int number;
    
            public FastProcessor(int number)
            {
                this.number = number;
            }
    
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void Process(int i)
            {
                number = ((number + i) / (number + i)) * number;
            }
        }
    
        static void Main(string[] args)
        {
            var sw1 = new Stopwatch();
            var processor1 = new FastProcessor(10);
            sw1.Start();
            for (int i = 1; i < 10000000; i++)
            {
                processor1.Process(i);
            }
            sw1.Stop();
    
            var sw2 = new Stopwatch();
            var processor2 = (IFastProcessor)new FastProcessorImpl(10);
            sw2.Start();
            for (int i = 1; i < 10000000; i++)
            {
                processor2.Process(i);
            }
            sw2.Stop();
    
            var number = 10;
            var sw3 = new Stopwatch();
            sw3.Start();
            for (int i = 1; i < 10000000; i++)
            {
                number = ((number + i) / (number + i)) * number;
            }
            sw3.Stop();
    
            Console.WriteLine($"Class: {sw1.ElapsedMilliseconds}ms, Interface: {sw2.ElapsedMilliseconds}ms, Inline: {sw3.ElapsedMilliseconds}ms");
        }
    }
    

    更新:我还尝试了一个带有虚拟方法的基类。令我非常惊讶的是,这也与内联版本执行相同,这意味着编译器可能正在优化虚拟调用,从而允许 JIT 内联。所以我不能确定接口与虚拟方法的问题。但是,另一方面,可以肯定地说,在 OPs 问题中,我看不出该方法不被内联的原因。

    【讨论】:

    • 亚当,这很有趣,谢谢。我注意到在您的测试代码中,可以从静态分析中看出 processor1 将始终是 FastProcessor 并且永远不会是任何其他具体类型。在那种情况下,我假设可能会发生内联。与处理器2相同的问题。我想知道你是否在 50% 的调用中调用了 processor1(作为 FastProcessor),然后将 fast Processor1 设置为 FastProcessorImpl 并在其余调用中再次调用它,你会注意到有什么不同吗?
    【解决方案3】:

    接口允许我们设计更好的代码,但在需要优化代码时会使事情复杂化。抖动(有时编译器也可以这样做)有一个技术库,在运行时使用这些技术来尝试看穿我们的代码并更好地执行。从 .NET Framework 5 开始,这些优化是在应用程序工作时完成的(如果抖动检测到性能不佳,可以重新应用它们)。如需了解它的功能,请查看RyuJIT Tutorial

    在高层讲话时,接口方法的调用通过 V-Table 调度。然而,在低级别,当抖动能够推断呼叫站点满足某些约束时,呼叫可以通过甚至被内联。这种技术称为去虚拟化。

    一般来说,如果 jit 可以在接口调用时确定 this 对象的类型,它可以去虚拟化,然后可能内联。确定类型的主要机制有两种:

    deduce the type from flow analysis within a method
    enable PGO, have that observe the possible types for this, and then test for the most likely type when rejitting or in a future run of the process.
    

    最后我看到流分析可以在相对较小的部分接口调用案例(比如不超过 10%)中启用去病毒化和内联。这里的成功需要接口站点上游有一些类型标识(构造函数调用或类型测试)的“有机”证据。内联可以帮助汇集所需的信息,但目前内联启发式不包括增加的去虚拟化潜力作为其评估的一部分。这可能很快就会改变(参见例如#53670)。

    PGO 在去虚拟化接口调用方面非常有效;我所做的大多数研究表明,超过 80% 的接口调用站点都有一个主要的实现类。 内联

    内联启发式算法复杂且难以简明概括。粗略地说,如果满足以下条件,方法将被内联:

    there is a direct call to the method, OR the jit can devirtualize an interface or virtual call, AND
        the method being invoked is small (16 bytes of IL or fewer), OR
        the method being invoked is marked with AggressiveInlining, OR
        the method is medium sized (17 to ~100 bytes of IL) and the inline heuristics determine the inline is worthwhile
    

    上述定义来自Andy Ayers,这是一个长期存在的问题,旨在提高这种情况下的性能(#7291)。

    随着运行时间和抖动随着时间的推移而改善,以前未优化的代码现在可以从某些优化中受益。事实上,它发生在一个月前,在即将发布的 framework 中进行了进一步改进。

    旁注

    微基准测试需要一定的技术和统计技能,因为很多事情都可能出错(例如噪声、处理器的动态频率、优化、代码预热......)。有些框架允许您在对静态更友好且可重复的环境中执行此类测量。 .NET 框架使用Benchmark .NET,它可以帮助您更好地理解您的代码。

    【讨论】:

      猜你喜欢
      • 2013-05-31
      • 2012-04-01
      • 2012-08-23
      • 1970-01-01
      • 2013-07-18
      • 1970-01-01
      • 1970-01-01
      • 2011-06-17
      • 1970-01-01
      相关资源
      最近更新 更多