我知道.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];
}
}
上述方法调用是否内联,即使它不是静态的并访问某个私有成员?
答案 0 :(得分:2)
我的结论是,实例方法可以内联,但虚方法不能,因为调用的实际方法可以在运行时更改,并且无法使用源代码的静态分析来建立。
由于这个原因,我在问题中显示的方法可以内联,如果它不是接口方法 - 因为这意味着它必须在运行时通过vtable查找调度它是虚拟的。 / p>
说,有一些JIT优化技术可以优化“常见”情况下的虚方法内联,但是当内联方法与运行时所需的方法调用不匹配时,这些技术会带来回退,这意味着某些代码路径可能会从其他人的内联中获益更多。
答案 1 :(得分:1)
好。我有结果。答案似乎是JIT可以内联一个实现接口并访问或修改类成员的方法。
我的结果是:
即。具有和不具有接口的相同性能。此外,如果没有编译器积极的内联指令,性能仍然相同。
测试代码:
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内联。所以我无法确定接口与虚拟方法的问题。但是,另一方面,可以肯定地说,在OP的问题中,我没有看到为什么该方法不会被内联的原因。
答案 2 :(得分:1)
接口让我们可以设计出更好的代码,但是当需要优化代码时,事情就变得复杂了。抖动(有时编译器也可以这样做)有很多在运行时使用的技术,可以尝试查看我们的代码并更好地执行。从 .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,它可以帮助您更好地理解您的代码。