0

I am developing a program that uses DynamicMethod quite a lot, and found that running it under Release mode is significantly slower than under Debug mode. I managed to repro the problem with the following small snippet.

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

public class Foo
{
    private static int Count = 0;

    public static void Increment()
    {
        Interlocked.Increment(ref Count);
    }

    public static int MyCount => Count;
}

public class Test
{
    private delegate void MyDelegate();

    private static MyDelegate Generate()
    {
        DynamicMethod test = new("test", null, Array.Empty<Type>());
        MethodInfo? m = typeof(Foo).GetMethod("Increment", Array.Empty<Type>());
        if (m == null) { throw new Exception("!!!"); }

        ILGenerator il = test.GetILGenerator(256);
        // By putting more EmitCalls, we see more differences
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.EmitCall(OpCodes.Call, m, null);
        il.Emit(OpCodes.Ret);

        return (MyDelegate) test.CreateDelegate(typeof(MyDelegate));
    }

    public static void Main()
    {
        Stopwatch sw = new();
        MyDelegate f = Generate();
        sw.Start();
        f();
        sw.Stop();
        Console.WriteLine("Time = {0:F6}ms", sw.Elapsed.TotalSeconds);
    }
}

When I run the above program in Debug mode and Release mode, the call takes about 0.0005ms and 0.0007ms, respectively. And of course, by making more EmitCalls, I can easily make it twice slower or more.

I am currently using .NET 6, and I see consistent behaviors in Windows, Linux, and macOS:

dotnet --version
6.0.203

I also tried to add GC.Collect before sw.Start() just to make sure that GC is not affecting the performance behavior. But I see the same differences. Am I missing anything here? Why is it slower in the Release mode?


@Hans answered in the comment that this is because JITting in the Release mode is slower than in the Debug mode due to extra optimizations.

I still would like to know if there is a way to turn off the optimizations specifically for DynamicMethods (while still being in the Release mode) because the jitting cost seems too high compared to the gain that I can get by repeatedly running the DynamicMethod.

skc
  • 121
  • 6
  • 1
    It is not slower, proper benchmarking is a fine art. The mistake is that the measured time includes the time needed to just-in-time compile the code. A one-time cost, always longer in the Release configuration, the jitter does more work to optimize the generated code. I measured 2.3 to jit and 0.0004 msec to execute in Debug, 12 and 0.0003 msec in Release. Measured by repeating the timing test 20 times. Best: https://benchmarkdotnet.org/ – Hans Passant May 17 '22 at 08:01
  • Thanks for the input. But could you tell me why the jitter does extra job for optimization when we provide raw instructions? I actually tried to dump the resulting byte arrays following "https://stackoverflow.com/questions/4146877/how-do-i-get-an-il-bytearray-from-a-dynamicmethod", but both Debug and Release modes show the same bytes, too. – skc May 17 '22 at 11:04
  • The job of the jitter is to convert those bytes into instructions that the processor can execute. More than one way to do that, making the generated machine code efficient instead of just direct translation as done in Debug requires extra work. https://stackoverflow.com/a/4045073/17034 – Hans Passant May 17 '22 at 11:33
  • Thanks for the clarification. Do you know if there is a way to turn off the optimizations in the release mode? – skc May 17 '22 at 11:44
  • You are already using it, the Debug build turns off optimizations. It is the defining difference between Debug and Release. https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debuggableattribute?view=net-6.0 – Hans Passant May 17 '22 at 11:52

1 Answers1

1

This answer targets your updated question. I tried your code and received similar results. Adding a MethodImplAttribute for both, your Generate and Increment method lead to almost identical results for the release configuration on my machine.

[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public static void Increment() { ... }

[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private static MyDelegate Generate() { ... }
wobuntu
  • 422
  • 5
  • 12