.netcilemitilasmno-op

Why does ILGenerator.Emit() insert nop opcodes in dynamic assembly?


I am building a small compiler in C#, so inevitably I had to meddle with dynamic assemblies and emitting opcodes. Now, the odd thing is that my Emit() calls create additional nop opcodes in the generated module. It is not so much essential in my case as performance is not really critical, but it honestly baffles me why this happens. It seems to be occuring after loading or storing to locals or arguments. Any C#/dynamic assembly pundit that could point me to things I could check? I have attached a sample of the generated code, if any more information is needed please let me know. Thanks.

IL_0000:  ldc.i4     0x0
IL_0005:  stloc      c
IL_0009:  nop
IL_000a:  nop
IL_000b:  ldloc      c
IL_000f:  nop
IL_0010:  nop
IL_0011:  stloc      i
IL_0015:  nop
IL_0016:  nop
IL_0017:  ldarg      s
IL_001b:  nop
IL_001c:  nop
IL_001d:  ldloc      i
IL_0021:  nop
IL_0022:  nop
IL_0023:  add
IL_0024:  stloc      $0
IL_0028:  nop
IL_0029:  nop
IL_002a:  ldloc      $0
IL_002e:  nop
IL_002f:  nop
IL_0030:  ldind.i1
IL_0031:  ldc.i4     0x0
IL_0036:  bne.un     IL_0040

IL_003b:  br         IL_008e

IL_0040:  ldloc      c
IL_0044:  nop
IL_0045:  nop
IL_0046:  stloc      $1

As requested, here follows an outline of what my code looks like. There are some things missing and since the code is divided into separate modules, these are the most relevant parts in the order they are executed.

string programName = "myprogram";

AssemblyBuilder assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName(programName), AssemblyBuilderAccess.RunAndSave);

ModuleBuilder module = n.AssemblyBuilder.DefineDynamicModule(programName, string.Format("{0}.exe", programName), true);

string contextName = string.Format("{0}.{1}", programName, "context");


MethodAttributes attributes = MethodAttributes.Private | MethodAttributes.Static;

MethodBuilder methodBuilder = typeBuilder.DefineMethod(method, attributes, returnType, paramTypes);

foreach (string name in paramNames)
    methodBuilder.DefineParameter(i++, ParameterAttributes.None, name);

ILGenerator Cil = methodBuilder.GetILGenerator();

...

foreach (var g in qLocals)
{
    LocalBuilder localBuilder = Cil.DeclareLocal(type);

    localBuilder.SetLocalSymInfo(g.Name);
}

foreach (var s in strings)
{
    LocalBuilder localBuilder = Cil.DeclareLocal(typeIndexed.DotNetElementType. MakePointerType());

    localBuilder.SetLocalSymInfo(string.Format("_{0}", index));
}

IEnumerable<Quad> jumpTargets =
    (from q in n.Tac
    select q.Addrs.OfType<AddrQuad>()).
    SelectMany(x => x).Select(a => a.Quad).Distinct();

    foreach (Quad q in jumpTargets)
        q.DefineLabel(Cil);
}

For each node on my abstract syntax tree (decorated with three address code), I simply do:

public override void DefaultPost(NodeBase n)
{
    foreach (Quad q in n.Tac)
        q.Emit(Cil);
}

This is a sequence of calls this function produces:

cil.Emit(OpCodes.Ldloc, Index);

cil.Emit(OpCodes.Stloc, Index);

cil.Emit(OpCodes.Ldc_I4, (int)this.i);

cil.Emit(OpCodes.Stloc, Index);

cil.Emit(OpCodes.Ldloc, Index);

cil.Emit(OpCodes.Ldc_I4, (int)this.i);

cil.Emit(OpCodes.Br, res.Quad.Label.Value);

cil.Emit(OpCodes.Ldloc, Index);

cil.Emit(OpCodes.Ldc_I4, (int)this.i);

cil.Emit(OpCodes.Stloc, Index);

cil.Emit(OpCodes.Ldloc, Index);

cil.Emit(OpCodes.Stloc, Index);

cil.Emit(OpCodes.Ldloc, Index);

cil.Emit(OpCodes.Ldc_I4, (int)this.i);

cil.Emit(OpCodes.Bge, quad.Label.Value);

cil.Emit(OpCodes.Br, res.Quad.Label.Value);

...

I don't know if this helps, should you want to check out my complete project, it is at:

http://github.com/yannikab/grc

Everything related to target code generation is under the Cil namespace. The class putting everything together for code generation is named CilVisitor.


Solution

  • As indicated in the comments, for the Ldarg, Stloc and Ldloc opcodes, you should be using the Emit overload that accepts a short as the second parameter, whereas your Index is presumably an int, so the wrong Emit overload is being used. The IL generator doesn't check this, and just outputs all 4 bytes of the value to the IL stream. The 2 high-order bytes are zero, which is nop in IL, hence the nops in your disassembly.

    Either change the type of Index to a short or cast it when passing to Emit.