【问题标题】:ILGenerator method inliningILGenerator 方法内联
【发布时间】:2011-12-31 00:05:24
【问题描述】:

给定以下代码:

using System;
using System.Reflection.Emit;
using System.Diagnostics;
using System.Reflection;

namespace ConsoleApplication1
{
    class A
    {
        public int Do(int n)
        {
            return n;
        }
    }

    public delegate int DoDelegate();

    class Program
    {
        public static void Main(string[] args)
        {
            A a = new A();

            Stopwatch stopwatch = Stopwatch.StartNew();
            int s = 0;
            for (int i = 0; i < 100000000; i++)
            {
                s += a.Do(i);
            }

            Console.WriteLine(stopwatch.ElapsedMilliseconds);
            Console.WriteLine(s);


            DynamicMethod dm = new DynamicMethod("Echo", typeof(int), new Type[] { typeof(int) }, true);
            ILGenerator il = dm.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ret);

            DynamicMethod dm2 = new DynamicMethod("Test", typeof(int), new Type[0]);
            il = dm2.GetILGenerator();


            Label loopStart = il.DefineLabel();
            Label loopCond = il.DefineLabel();

            il.DeclareLocal(typeof(int));   // i
            il.DeclareLocal(typeof(int));   // s

            // s = 0;
            il.Emit(OpCodes.Ldc_I4_0);
            il.Emit(OpCodes.Stloc_1);

            // i = 0;
            il.Emit(OpCodes.Ldc_I4_0);
            il.Emit(OpCodes.Stloc_0);

            il.Emit(OpCodes.Br_S, loopCond);

            il.MarkLabel(loopStart);

            // s += Echo(i);
            il.Emit(OpCodes.Ldloc_1);   // Load s
            il.Emit(OpCodes.Ldloc_0);   // Load i
            il.Emit(OpCodes.Call, dm);  // Call echo method
            il.Emit(OpCodes.Add);
            il.Emit(OpCodes.Stloc_1);

            // i++
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ldc_I4_1);
            il.Emit(OpCodes.Add);
            il.Emit(OpCodes.Stloc_0);

            il.MarkLabel(loopCond);

            // Check for loop condition
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ldc_I4, 100000000);
            il.Emit(OpCodes.Blt_S, loopStart);

            il.Emit(OpCodes.Ldloc_1);
            il.Emit(OpCodes.Ret);


            DoDelegate doDel = (DoDelegate)dm2.CreateDelegate(typeof(DoDelegate));
            s = doDel.Invoke();     // Dummy run to force JIT


            stopwatch = Stopwatch.StartNew();
            s = doDel.Invoke();
            Console.WriteLine(stopwatch.ElapsedMilliseconds);
            Console.WriteLine(s);
        }
    }
}

对方法 Do 的调用被内联。循环在大约 40 毫秒内完成。例如,如果我将 Do 设为虚函数,它不会被内联,并且循环在 240 毫秒内完成。到现在为止还挺好。当我使用 ILGenerator 生成 Do 方法(Echo),然后使用与给定 main 方法相同的循环生成 DynamicMethod 时,对 Echo 方法的调用永远不会内联,并且循环完成大约需要 240 毫秒。 MSIL 代码是正确的,因为它返回与 C# 代码相同的结果。我确信方法内联是由 JIT 完成的,所以我认为没有理由不内联 Echo 方法。

有谁知道为什么这个简单的方法不会被 JIT 内联。

【问题讨论】:

  • 您是否也在生成调用动态生成的 Do() 方法的代码,或者该代码在编译时是否已知?
  • 您能否包含使用 ILGenerator 的完整代码示例?而且,为了确定:您是否在发布版本下测试没有附加调试器?
  • 我重新编辑了帖子,提供了测试应用程序的完整代码。我使用发布版本并在没有调试器的情况下运行它。 C# for 循环内联方法调用,运行速度明显快于 IL for 循环。

标签: .net methods inline cil ilgenerator


【解决方案1】:

经过进一步调查,我得出以下结论:

  1. ILGenerator 生成的方法永远不会被内联。无论您是使用委托、从另一个 DynamicMethod 还是从使用 MethodBuilder 创建的方法调用它们都没有关系。
  2. 现有方法(用 C# 编码并由 VS 编译的方法)只有在从使用 MethodBuilder 创建的方法调用时才能内联。如果从 DynamicMethod 调用,它们将永远不会被内联。

在彻底测试了许多样本并查看了最终的汇编代码后,我得出了这一结论。

【讨论】:

  • 直到有人表现不同,我认为这是一个准确的结论。不内联动态方法有时会有意义(这是我在回复中建议的)。也许编译器设计者决定将其视为所有情况的规则是最简单的。我想知道表达式树的行为是否相同:msdn.microsoft.com/en-us/library/bb397951.aspx
  • 检查一下会很有趣,虽然我没有时间(或不需要)玩表达式树。
  • 确定这也包括生成并保存为 dll 并在保存后加载的 IL 代码?因为我确定它不包括已保存的生成程序集。无论如何,如果您的答案是正确的,您可以将其标记为正确。
【解决方案2】:

如果我理解正确,我的猜测是,由于该方法是动态生成的,JIT 编译器不知道为调用者内联它。

我已经编写了大量的 IL,但我没有研究过内联行为(主要是因为动态方法对于我的目的来说通常足够快,无需进一步优化)。

我欢迎对此主题更了解的人提供反馈(请不要只是投反对票;如果我错了,我想在这里学习一些东西)。

非动态

  • 您编写“普通”的 .NET 代码(例如 C#、VB.NET、任何支持 CLS 的语言)
  • IL 在编译时创建
  • 机器码是在运行时创建的;方法在适当的地方被内联

动态

  • 您编写“普通”.NET 代码,其目的是创建动态方法
  • 此代码在编译时创建 IL,但未创建动态方法
  • 机器码是在运行时为生成动态方法的代码创建的
  • 当调用该代码时,动态方法在特殊程序集中创建为 IL
  • 动态方法的 IL 被编译为机器码
  • 但是,JIT 编译器不会重新编译其他调用者来内联新的动态方法。或者,其他调用者本身可能是动态的,尚未创建。

【讨论】:

  • 我已经添加了重现问题的完整示例代码。如您所见,我生成了两种动态方法。一个非常简单,只需返回与参数相同的值。另一种方法运行一个简单的 for 循环(与开头的 C# 代码相同)。我不希望 JIT 内联对第二种方法的调用,因为它是通过委托调用的。然而,我确实希望他内联第一种方法,当它在第二种方法中调用时。当我自己内联它时,相同循环的执行速度提高了 10 倍。
【解决方案3】:

您可以尝试生成动态程序集。我的理解是大多数运行时都不知道它是动态的。我认为在内部它会像任何其他 byte[] 程序集一样被加载。因此 JIT/inliner 也可能不知道(或不在乎)。

【讨论】:

  • 我什至试过了。不知何故,出于某种原因,动态生成的方法(无论你如何生成它)都不会被内联。无论如何,谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-01-02
  • 2012-04-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-10
相关资源
最近更新 更多