6

I am porting an app from .NET Framework to .NET Core, which used runtime code generation.

Here's the OLD code:

var refs = new []
{
    typeof(Foo).Assembly.Location,
    typeof(Enumerable).Assembly.Location
};
var options = new CompilerParameters(refs)
{
    GenerateExecutable = false,
    GenerateInMemory = true
};
var provider = new CSharpCodeProvider();
var result = provider.CompileAssemblyFromSource(options, rawText);
return result.CompiledAssembly;

The new code, using Roslyn, looks like this:

var root = Path.GetDirectoryName(typeof(object).Assembly.Location);
var typeRefs = new[]
{
    typeof(object).Assembly.Location,
    Path.Combine(root, "System.Collections.dll"),
    Path.Combine(root, "System.Runtime.dll"),
    typeof(Foo).Assembly.Location,
    typeof(Enumerable).Assembly.Location
};

var source = Measure("SourceText", () => SourceText.From(rawText));
var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7);
var syntaxTree = Measure("Parse", () => SyntaxFactory.ParseSyntaxTree(source, parseOptions));

var compilation = Measure("Compile", () => CSharpCompilation.Create(
    "Bar.dll",
    new[] {syntaxTree},
    typeRefs.Select(x => MetadataReference.CreateFromFile(x)),
    new CSharpCompilationOptions(
        OutputKind.DynamicallyLinkedLibrary,
        optimizationLevel: OptimizationLevel.Debug
    )
));

using var stream = new MemoryStream();

var emitResult = Measure("Emit", () => compilation.Emit(stream));
if(!emitResult.Success)
    throw new Exception("Failed to compile parser code!");

stream.Seek(0, SeekOrigin.Begin);
return Assembly.Load(stream.ToArray());

The problem is: the new code works at least 2x slower. I used a very simple Measure function to track down the bottleneck:

private static T Measure<T>(string caption, Func<T> generator)
{
    var now = DateTime.Now;
    var result = generator();
    var end = DateTime.Now - now;
    Debug.WriteLine($"{caption}: {end.TotalMilliseconds} ms");
    return result;
}

And here's what it prints:

SourceText: 4,7926 ms
Parse: 598,5749 ms
Compile: 43,4261 ms
Emit: 5256,0411 ms

As you can see, generating the assembly via compilation.Emit takes a whopping 5.2 seconds.

Is there any way to speed this up? Maybe some flags in the options that affect compilation time? I tried the following, without any success:

  • Changing between OptimizationLevel.Debug and OptimizationLevel.Release
  • Splitting the single source to multiple source parts, one per class (this actually made compilation slower)

Any ideas?

Impworks
  • 2,647
  • 3
  • 25
  • 53
  • 2
    I recently tried the same thing, migrating a program that generates and compiles code using CodeDom to using Roslyn instead. I'm seeing the same issue, but the slow performance only occurs on the first compile after program launch. Further compiles are instantaneous. So it's probably some sort of one-time initialization of the Roslyn library. Don't know if there's any way to speed that up, but one idea is to compile some "hello world" code in the background during the app's splash screen, so the user sees no delay when they later trigger a compile of real code. – Karl von L Apr 06 '22 at 15:38

0 Answers0