7

我注意到 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

4 回答 4

2

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

        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 通过内存流提供什么的证据。

于 2013-07-23T23:12:39.353 回答
1

我使用 ASP.net 的Microsoft.CodeDom.Providers.DotNetCompilerPlatform包遇到了同样的问题。事实证明,这个包启动了 csc.exe,它使用 VBCSCompiler.exe 作为编译服务器。默认情况下,VBCSCompiler.exe 服务器的生存时间为 10 秒,其启动时间约为 3 秒。这解释了为什么运行代码一次或多次需要大约相同的时间。微软似乎也在 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);
    }
于 2016-09-09T19:55:23.043 回答
0

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

于 2014-01-21T19:03:33.977 回答
0

tldr:NGEN-ing roslyn dlls 将初始编译/执行时间缩短了 1.5 秒(在我的情况下,从 ~2 秒到 ~0.5 秒)


刚刚调查了这个。

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

Perfview 透露延迟主要是由于抖动:

在此处输入图像描述

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

我在 Microsoft.CodeAnalysis.CSharp.dll 上使用了 ngen(由于 app.config 中的绑定重定向,请确保指定 /ExeCondig:MyApplication.exe)并获得了不错的性能改进,首次执行时间降至约 580 毫秒。

这当然需要在最终用户机器上完成。就我而言,我使用 Wix 作为我的软件的安装程序,并且在安装时支持 NGEN-ing文件。

于 2017-10-02T10:39:22.227 回答