我知道静态方法可以通过.Net (和Mono)中的即时优化进行内联。
我的问题是,访问自己状态的实例方法也可以内联吗?
例如:
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];
}
}上面的方法调用是内联的吗,即使它不是静态的并且访问一些私有成员?
发布于 2017-12-22 08:51:34
我在这里找到了一个很棒的读物:http://blogs.microsoft.co.il/sasha/2007/02/27/jit-optimizations-inlining-and-interface-method-dispatching-part-1-of-n/
我的结论是,实例方法可以内联,但虚方法不能,因为实际调用的方法可以在运行时更改,并且不能使用源代码的静态分析来建立。
出于这个原因,我在问题中展示的方法可以是内联的,如果它不是一个接口方法-因为这意味着它是虚拟的,因为它必须在运行时通过vtable查找来调度。
也就是说,有一些JIT优化技术可以优化“常见”情况下的虚方法内联,但当内联方法在运行时与所需的方法调用不匹配时,这些技术会带来后备,这意味着某些代码路径可能比其他代码路径从内联中受益更多。
发布于 2017-12-22 09:12:04
好吧。我有结果了。答案似乎是,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内联。所以在接口和虚方法的问题上我不能确定。但是,另一方面,可以肯定地说,在操作问题中,我看不出为什么该方法不是内联的。
发布于 2021-07-08 15:07:06
接口允许我们设计更好的代码,但当代码需要优化时,会使问题复杂化。抖动(有时编译器也可以做到这一点)有大量的技术,这些技术在运行时被用来试图看透我们的代码并更好地执行。从.NET Framework5开始,这些优化是在应用程序工作时完成的(如果抖动检测到性能不佳,则可以重新应用这些优化)。要了解它的功能,请看一下RyuJIT Tutorial。
当使用高级语言时,接口方法上的调用通过V-Table进行调度。然而,在较低的级别,当抖动能够推断出调用位置满足某些约束时,调用可以通过,甚至是内联。这种技术称为去虚拟化。
通常,如果jit可以在接口调用时确定
对象的类型,它就可以解除虚拟化,然后潜在地内联。确定类型有两种主要机制:
在启用PGO的方法中从流分析中推断类型,让观察可能的类型,然后在重新启动时或在流程的未来运行中测试最可能的类型。
最后,我认为流程分析可以在相对较小的接口调用情况下(比如不超过10%)实现离散化和内联。这里的成功要求在接口站点的上游有一些类型标识(构造函数调用或类型测试)的“有机”证据。内联可以帮助集中所需的信息,但目前内联启发式方法不包括增加的去虚拟化潜力作为其评估的一部分。这种情况可能很快就会改变(参见#53670)。
PGO在解除接口调用虚拟化方面非常有效;我所做的大多数研究表明,80%以上的接口调用站点都有一个主要的实现类。内联
内联启发式很复杂,很难简明扼要地总结。粗略地说,如果满足以下条件,方法将被内联:
存在对方法的直接调用,或者jit可以解除接口或虚拟调用的虚拟化,并且被调用的方法很小(16字节的IL或更少),或者被调用的方法被标记为AggressiveInlining,或者该方法是中等大小的(17到~100字节的IL),并且内联启发式确定内联是值得的
上面的定义来自于Andy Ayers在一个长期存在的问题上,该问题旨在提高像这样的情况下的性能(#7291)。
随着运行时和抖动随着时间的推移而改善,以前未优化的代码现在可以受益于某些优化。事实上,它发生在一个月前,在即将到来的framework中进行了额外的改进。
附注
微基准测试需要一定的技术和统计技能,因为很多事情都可能出错(例如,噪声、处理器的动态频率、优化、代码预热...)。有一些框架允许您在更静态友好和可重复的环境中执行此类测量。.NET框架使用Benchmark .NET,它可以帮助你更好地理解你的代码。
https://stackoverflow.com/questions/47934427
复制相似问题