1

I'm learning IL and I thought of writing kind of a high-performance hack to access a field values of any object (like a reflection but faster).

So I made this class for testing:

public class CrashTestDummy
{
    public int Number { get; set; }

    public CrashTestDummy(int number)
    {
        Number = number;
    }

    public override string ToString()
    {
        return string.Format("CrashTestDummy: Number = {0}", Number);
    }
}

Then I have such a program (I added comments after all IL instructions to improve readability, also divided into several logical parts; after every part there is written what I think is now on the stack):

class Program
{
    static void Main(string[] args)
    {
        var backingFieldFormat = "<{0}>k__BackingField";
        var getPropFormat = "get_{0}";

        var dummy = new CrashTestDummy(5);

        var t = dummy.GetType();
        var f = t.GetField(string.Format(backingFieldFormat, "Number"),
            BindingFlags.Instance | BindingFlags.NonPublic); 

        // define method: object Getter(Type, FieldInfo, Object), ignoring private fields.
        var getter = new DynamicMethod("Getter", typeof(object), new Type[] { typeof(Type), typeof(FieldInfo), typeof(object) }, true);
        var il = getter.GetILGenerator();
        var _t = il.DeclareLocal(typeof(Type));      // Type _t;
        var _f = il.DeclareLocal(typeof(FieldInfo)); // FieldInfo _f;
        var _ft = il.DeclareLocal(typeof(Type));     // Type _ft;
        var get_FieldType = typeof(FieldInfo).GetMethod(string.Format(getPropFormat, "FieldType")); // MethodInfo for FieldInfo.FieldType getter
        var get_IsValueType = typeof(Type).GetMethod(string.Format(getPropFormat, "IsValueType"));  // MethodInfo for Type.IsValueType getter
        var lbl_NotValueType = il.DefineLabel();       // label "NotValueType"

        // PART 1.
        il.Emit(OpCodes.Ldarg_0);                      // Get argument 0 (type of object) ...
        il.Emit(OpCodes.Castclass, typeof(Type));      // ... cast it to Type (just in case) ...
        il.Emit(OpCodes.Stloc, _t);                    // ... and assign it to _t.
        il.Emit(OpCodes.Ldarg_1);                      // Get argument 1 (desired field of object) ...
        il.Emit(OpCodes.Castclass, typeof(FieldInfo)); // ... cast it to FieldInfo (just in case) ...
        il.Emit(OpCodes.Stloc, _f);                    // ... and assign it to _f.
        // stack is empty

        // DEBUG PART
        il.EmitWriteLine(_t);           // these two lines show that both
        il.EmitWriteLine(t.ToString()); // t and _t contains equal Type
        il.EmitWriteLine(_f);           // these two lines show that both
        il.EmitWriteLine(f.ToString()); // f and _f contains equal FieldInfo
        // stack is empty

        // PART 2.
        il.Emit(OpCodes.Ldarg_2);       // Get argument 2 (object itself) ...
        il.Emit(OpCodes.Castclass, _t); // ... cast it to type of object ...
        il.Emit(OpCodes.Ldfld, _f);     // ... and get it's desired field's value.
        // desired field's value on the stack

        // PART 3.
        il.Emit(OpCodes.Ldloc, _f);                 // Get FieldInfo ...
        il.Emit(OpCodes.Call, get_FieldType);       // ... .FieldType ...
        il.Emit(OpCodes.Call, get_IsValueType);     // ... .IsValueType; ...
        il.Emit(OpCodes.Brfalse, lbl_NotValueType); // ... IF it's false - goto NotValueType.
            il.Emit(OpCodes.Ldloc, _f);             // Get FieldInfo ...
            il.Emit(OpCodes.Call, get_FieldType);   // ... .FieldType ...
            il.Emit(OpCodes.Stloc, _ft);            // ... and assign it to _ft.
            il.Emit(OpCodes.Box, _ft);              // Box field's value of type _ft.
        il.MarkLabel(lbl_NotValueType);             // NotValueType:
        // desired field's value on the stack (boxed, if it's value type)

        // PART 4.
        il.Emit(OpCodes.Ret); // return.            

        var value = getter.Invoke(null, new object[] { t, f, dummy });
        Console.WriteLine(value);
        Console.ReadKey();
    }
}

This code crashes (on Invoke, and Exception from within Emit is as helpful as always). I can replace PARTs 2. and 3. as below:

        // ALTERNATE PART 2.
        il.Emit(OpCodes.Ldarg_2);      // Get argument 2 (object itself) ...
        il.Emit(OpCodes.Castclass, t); // ... cast it to type of object ...
        il.Emit(OpCodes.Ldfld, f);     // ... and get it's desired field's value.
        // desired field's value on the stack

        // ALTERNATE PART 3.
        if (f.FieldType.IsValueType)           
            il.Emit(OpCodes.Box, f.FieldType); // Box field's value of type f.FieldType.
        // desired field's value on the stack (boxed, if it's value type)

and it works fine. Notice that this time I'm not using any local variables, f and t are variables from outside of method. However with this approach I would need to generate as many methods, as number of types and fields it was being used to. So it's pretty unsatisfying solution.

I'm doing something wrong with local variables, apparently, but I was unable to figure out what it is exactly. What am I missing?


Edit:

Here's the code after big simplification. CrashTestDummy has now string property, so I could get rid of boxing the int:

public class CrashTestDummy
{
    public string Text { get; set; }

    public CrashTestDummy(string text)
    {
        Text = text;
    }
}

And the main code is as follows:

    static string BackingField(string propertyName)
    {
        return string.Format("<{0}>k__BackingField", propertyName);
    }

    static void Main(string[] args)
    {
        // INIT
        var dummy = new CrashTestDummy("Loremipsum");
        var t = typeof(CrashTestDummy);
        var f = t.GetField(BackingField("Text"),
            BindingFlags.Instance |
            BindingFlags.Public |
            BindingFlags.NonPublic);

        var fieldGetter = new DynamicMethod("FieldGetter", typeof(object), new Type[] { typeof(object) }, true);
        var il = fieldGetter.GetILGenerator();

        // DYNAMIC METHOD CODE
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Castclass, t);
        il.Emit(OpCodes.Ldfld, f);
        il.Emit(OpCodes.Ret);

        var d = (Func<object, object>)fieldGetter.CreateDelegate(typeof(Func<object, object>));

        // BENCHMARK
        Stopwatch sw = new Stopwatch();
        var len = 1000000;
        for (int i = 0; i < len; i++)
        {
            sw.Start();
            d(dummy);
            sw.Stop();
        }
        Console.WriteLine(sw.Elapsed);
        sw.Reset();
        for (int i = 0; i < len; i++)
        {
            sw.Start();
            f.GetValue(dummy);
            sw.Stop();
        }
        Console.WriteLine(sw.Elapsed);

        Console.ReadKey();
    }
Sushi271
  • 544
  • 2
  • 8
  • 22
  • 3
    Have you considered using sigil ? the errors from that are intentionally much clearer - and errors are usually flagged *when you make them* (during emit, not during invoke)... http://www.nuget.org/packages/Sigil/ – Marc Gravell Apr 03 '14 at 10:08
  • 4
    Maybe I'm missing something, but it sure looks like you are doing it wrong. There's no point in generating IL that uses Reflection, it will be just as slow as writing it in C# code. The point is that you use it when you generate the code, generating different code for different Types. – Hans Passant Apr 03 '14 at 10:35
  • @MarcGravell Wow, it looks great, but what about it's performance? If it does so much error checking, it's surely a bit slower. Though I will test it, thanks. – Sushi271 Apr 03 '14 at 10:43
  • @HansPassant Hmmm. You say so? If I had a working code, I would do some performance tests of course. That was my plan all along. So I still want to make this code working, so I could see how much time does it take on my own eyes; try some variations. – Sushi271 Apr 03 '14 at 10:46
  • 1
    If you generate the same code in IL that the C# compiler would have generated you will get the same level of performance. No point in doing that. Generate specialized code instead which is what the C# compiler cannot do. – usr Apr 03 '14 at 11:06
  • What about if I wanted to lazy-generate methods for any fields that can occur? I mean that there could be `Dictionary>>`. That could be done with this alternative way I provided in my question. I even made some performance tests with this working version of method. It's only about 2x faster than ordinary `FieldInfo.GetValue(object)`. Is it because I still use `Type` and `FieldInfo` inside? But how am I supposed to use e.g. `OpCodes.Ldfld` without providing some FieldInfo? – Sushi271 Apr 03 '14 at 11:54

1 Answers1

3

What you could do is create a specialized accessor function on the first use of any given FieldInfo. This removes reflection costs for subsequent accesses and replaces them with the cost of a delegate call which is much cheaper.

It's only about 2x faster than [reflection]

I would doubt this benchmark result. How could it be faster? If you generate the same code in IL that the C# compiler would have generated you will get the same level of performance. No point in doing that.

usr
  • 168,620
  • 35
  • 240
  • 369
  • Perhaps it's not the same. I used Stopwatch to measure each usage of method. I even made CrashTestDummy's property to be String, so I wouldn't need to box it. My method's code is only 4 lines now: `il.Emit(OpCodes.Ldarg_0);`, `il.Emit(OpCodes.Castclass, t);`, `il.Emit(OpCodes.Ldfld, f);`, `il.Emit(OpCodes.Ret);` – Sushi271 Apr 03 '14 at 12:01
  • This is very different IL than you gave in the question. This makes sense now. – usr Apr 03 '14 at 12:05
  • Well, when I used those `ALTERNATE` parts, there was no need for `_t`, `_f`, `_ft`, as well as those `get_xyz` methods so I just got rid of them. And the `PART 1` went to trash also, cause it was pointless without them. And without boxing I also got rid of `PART 3`, so the code from my comment above is all what's left. But it's still only 2x faster. – Sushi271 Apr 03 '14 at 12:10
  • That sounds suspiciously little to me. Can you post the IL generation code and the benchmark code? Edit it into the question. – usr Apr 03 '14 at 12:11
  • OK, one more thing: previously I measured time on Debug mode and with debugger (F5). On Release and without debugger (Ctrl+F5) it gives a 5x time improvement. I.e. `FieldInfo.GetValue` repeated 1,000,000 times takes about 0.5s, whereas my IL method lasts about 0.1s. – Sushi271 Apr 03 '14 at 12:23
  • 1
    Move the Stopwatch code out of the loop. You are measuring stopwatch overhead. Wrap all of it in a for loop that runs 5 times. Make sure, the times are stable. Discard the first time because you'd measure all one-time initialization and compilation. Debug mode means nothing.; That said, the benchmark is mostly valid and 5x speedup approaches a realistic value. I had estimated between 10x and 100x speedup. – usr Apr 03 '14 at 12:29
  • 1
    Whoa! I did as you said and its about 20x better on Release+Ctrl+F5. So my measurement was just unreliable. So now reflection takes between 260 and 340ms, whereas IL takes between 12 and 18ms. PS. Of course I'm talking about 1 million of runs. – Sushi271 Apr 03 '14 at 12:41