罗斯林启动时间慢

时间:2013-03-12 15:31:14

标签: c# roslyn

我注意到Roslyn解析/编译的启动时间是相当重要的一次性成本。编辑:我正在使用Roslyn CTP MSI(程序集在GAC中)。这是预期的吗?有没有解决方法?

运行下面的代码与1次迭代(约3秒)和300次迭代(约3秒)的时间几乎相同。

[Test]
public void Test()
{
    var iters = 300;
    foreach (var i in Enumerable.Range(0, iters))
    {
        // Parse the source file using Roslyn
        SyntaxTree syntaxTree = SyntaxTree.ParseText(@"public class Foo" + i + @" { public void Exec() { } }");

        // Add all the references we need for the compilation
        var references = new List<MetadataReference>();
        references.Add(new MetadataFileReference(typeof(int).Assembly.Location));

        var compilationOptions = new CompilationOptions(outputKind: OutputKind.DynamicallyLinkedLibrary);

        // Note: using a fixed assembly name, which doesn't matter as long as we don't expect cross references of generated assemblies
        var compilation = Compilation.Create("SomeAssemblyName", compilationOptions, new[] {syntaxTree}, references);

        // Generate the assembly into a memory stream
        var memStream = new MemoryStream();

        // if we comment out from this line and down, the runtime drops to ~.5 seconds
        EmitResult emitResult = compilation.Emit(memStream);

        var asm = Assembly.Load(memStream.GetBuffer());
        var type = asm.GetTypes().Single(t => t.Name == "Foo" + i);
    }
}

4 个答案:

答案 0 :(得分:2)

我认为一个问题是使用内存流,而应该尝试使用动态模块和ModuleBuilder。总体而言,代码执行速度更快,但仍然有较重的首次加载方案。我自己对罗斯林很陌生,所以我不确定为什么这会更快,但这里是改变后的代码。

        var iters = 300;
        foreach (var i in Enumerable.Range(0, iters))
        {
            // Parse the source file using Roslyn
            SyntaxTree syntaxTree = SyntaxTree.ParseText(@"public class Foo" + i + @" { public void Exec() { } }");

            // Add all the references we need for the compilation
            var references = new List<MetadataReference>();
            references.Add(new MetadataFileReference(typeof(int).Assembly.Location));

            var compilationOptions = new CompilationOptions(outputKind: OutputKind.DynamicallyLinkedLibrary);

            // Note: using a fixed assembly name, which doesn't matter as long as we don't expect cross references of generated assemblies
            var compilation = Compilation.Create("SomeAssemblyName", compilationOptions, new[] { syntaxTree }, references);

            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new System.Reflection.AssemblyName("CustomerA"),
            System.Reflection.Emit.AssemblyBuilderAccess.RunAndCollect);

            var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule");

            System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
            watch.Start();

            // if we comment out from this line and down, the runtime drops to ~.5 seconds
            var emitResult = compilation.Emit(moduleBuilder);

            watch.Stop();

            System.Diagnostics.Debug.WriteLine(watch.ElapsedMilliseconds);

            if (emitResult.Diagnostics.LongCount() == 0)
            {
                var type = moduleBuilder.GetTypes().Single(t => t.Name == "Foo" + i);

                System.Diagnostics.Debug.Write(type != null);
            }
        }

通过使用这种技术,编译只需96毫秒,在后续迭代中需要大约3到15毫秒。所以我认为你可能在第一个负载场景方面增加一些开销。

抱歉,我无法解释为什么它更快!我本人正在研究Roslyn,今晚稍后会进行更多的挖掘,看看我是否能够找到更多关于ModuleBuilder在内存流上提供的证据。

答案 1 :(得分:1)

我使用ASP.net的Microsoft.CodeDom.Providers.DotNetCompilerPlatform包遇到了同样的问题。事实证明,这个软件包启动了csc.exe,它使用VBCSCompiler.exe作为编译服务器。默认情况下,VBCSCompiler.exe服务器存活10秒,其启动时间约为3秒。这解释了为什么一次或多次运行代码需要大约相同的时间。看起来Microsoft在Visual Studio中也使用此服务器,以避免每次运行编译时都支付额外的启动时间。

使用此程序包您可以监视您的进程,并找到一个类似于csc.exe / keepalive的命令行:10

好的部分是,如果此服务器保持活动状态(即使在应用程序的两个会话之间),您也可以一直进行快速编译。

不幸的是,Roslyn包不是真正可定制的,我发现改变这个keepalive常量的最简单方法是使用反射来设置非公共变量值。在我这边,我把它定义为一整天,因为它总是保持相同的服务器,即使我关闭并重新启动我的应用程序。

    /// <summary>
    /// Force the compiler to live for an entire day to avoid paying for the boot time of the compiler.
    /// </summary>
    private static void SetCompilerServerTimeToLive(CSharpCodeProvider codeProvider, TimeSpan timeToLive)
    {
        const BindingFlags privateField = BindingFlags.NonPublic | BindingFlags.Instance;

        var compilerSettingField = typeof(CSharpCodeProvider).GetField("_compilerSettings", privateField);
        var compilerSettings = compilerSettingField.GetValue(codeProvider);

        var timeToLiveField = compilerSettings.GetType().GetField("_compilerServerTimeToLive", privateField);
        timeToLiveField.SetValue(compilerSettings, (int)timeToLive.TotalSeconds);
    }

答案 2 :(得分:0)

当您调用Compilation.Emit()时,它是您第一次真正需要元数据,因此会发生元数据文件访问。之后,它的缓存。虽然这不应该只为mscorlib占3secs。

答案 3 :(得分:0)

tldr:NGEN-ing roslyn dlls在初始编译/执行时间内减少1.5秒(在我的情况下从~2s到~0.5s)

刚才调查了这一点。

使用全新的控制台应用程序和Microsoft.CodeAnalysis.Scripting的nuget引用,小片段(&#34; 1 + 2&#34;)的初始执行大约需要2秒,而后续的执行速度要快得多 - 大约80毫秒(我的口味仍然有点高,但这是一个不同的主题)。

Perfview透露,延迟主要是由于咬合:

enter image description here

  • Microsoft.CodeAnalysis.CSharp.dll:941ms(3,205种方法)
  • Microsoft.CodeAnalysis.dll 426ms(1,600种方法)

我在Microsoft.CodeAnalysis.CSharp.dll上使用了ngen(确保指定/ExeCondig:MyApplication.exe,因为app.config中的绑定重定向)并获得了不错的性能提升,第一次执行时间降到了〜580ms。

这当然需要在最终用户机器上完成。在我的情况下,我使用Wix作为我的软件的安装程序,并在安装时使用support for NGEN-ing个文件。