Time to be a little more dynamic

C# 4 added support for the new dynamic type, which uses a version of the DLR to dispatch calls at runtime, allowing objects to change their behaviour quite dramatically.

At the C# level, it is relatively easy to implement a dynamic object by inheriting from the DynamicObject class. This convenience class makes it really easy to implement dynamic objects, though to really see what’s going on one needs to look at the IDynamicMetaObjectProvider interface.

To do a little experimentation, I defined the following class:

    class MyBaseObject : DynamicObject
    {
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            result = "Object1";
            return true;
        }
    }

This dynamic object is going to return the string "Object1" from any member call that is made on it.

In order to study the call site caching that happens, I also defined the following classes.

    class MyObject1 : MyBaseObject {}
    class MyObject2 : MyBaseObject {}
    class MyObject3 : MyBaseObject {}
         …
    class MyObject11 : MyBaseObject {}

We’ll use the following test program to drive the creation of the dynamic objects.

            object[] targets = { new MyObject1(), new MyObject1(), new MyObject1(),
                                 new MyObject2(), new MyObject1(), new MyObject2(),
                                 new MyObject3(), new MyObject4(), new MyObject5(),
                                 new MyObject6(), new MyObject7(), new MyObject8(),
                                 new MyObject9(), new MyObject10(), new MyObject1()
                               };
            foreach (dynamic target in targets)
            {
                dynamic result = target.Foo;
            }

Using Reflector, with the optimization set at 2.0, we see how the dynamic call is implemented. The foreach loop in the above translates to the following code.

foreach (object obj2 in objArray)
    {
        if (<Main>o__SiteContainer0.<>p__Site1 == null)
        {
            <Main>o__SiteContainer0.<>p__Site1 =
                    CallSite<Func<CallSite, object, object>>.Create(
                              Binder.GetMember(CSharpBinderFlags.None, "Foo", typeof(Program),
                                new CSharpArgumentInfo[] {
                                     CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
                                      }));
        }
        object obj3 = <Main>o__SiteContainer0.<>p__Site1.Target(<Main>o__SiteContainer0.<>p__Site1, obj2);
    }

I wanted to look at the dynamic behaviour, so the next thing to do was to ensure that the assembly containing the interesting code is unoptimised so that I could step through it using Reflector Pro. We can do this by uninstalling the default optimised ngen version.

  c:WindowsMicrosoft.NETFrameworkv4.0.30319ngen.exe uninstall System.Core.dll

Create an ini file to turn off optimization

>type System.Core.ini
[.NET Framework Debugging Control]
GenerateTrackingInfo=1
AllowOptimize=0

And then ngen the assembly again. This time the ini file will ensure that there is no optimisation.

  c:WindowsMicrosoft.NETFrameworkv4.0.30319ngen.exe install System.Core.dll

If you use Reflector to follow the code from CallSite<.>.Create, you see that the Target property holds a delegate that calls UpdateAndExecute1. I therefore decompiled System.Core using Reflector Pro and navigated to the UpdateDelegates class where I put a breakpoint on UpdateAndExecute1. On the first call to target.Foo, with target bound to MyObject1, we enter this method.

For a given call site, the system maintains some caches that hold dispatches that we’ve made in the past at this call site. On the first call, we drop through to

         func = site2.Target = site2.Binder.BindCore<Func<CallSite, T0, TRet>>(site2, args);

which is code that is going to compile a test and fast dispatch to a given set of typed arguments.

The code passes through a set of methods until it eventually reaches the Compile method in System.Linq.Expression.Expression<T>. Grabbing the expression tree, by going to the locals window and using "Make ObjectId", we can later look at the code to determine that it is compiling the following delegate. The debugger has a view for exposing Expression Trees in the textual format seen below.

.Lambda CallSite.Target<System.Func`3[System.Runtime.CompilerServices.CallSite,System.Object,System.Object]>(
    System.Runtime.CompilerServices.CallSite $$site,
    System.Object $$arg0) {
    .Block() {
        .If (
            $$arg0 .TypeEqual ConsoleApplication14.MyObject1
        ) {
            .Return #Label1 { .Block(System.Object $var1) {
                .If (
                    .Call ((System.Dynamic.DynamicObject)$$arg0).TryGetMember(
                        .Constant<System.Dynamic.GetMemberBinder>(Microsoft.CSharp.RuntimeBinder.CSharpGetMemberBinder),
                        $var1)
                ) {
                    $var1
                } .Else {
                    .Throw .New Microsoft.CSharp.RuntimeBinder.RuntimeBinderException("’ConsoleApplication14.MyObject1′ does not contain a definition for ‘Foo’")
                }
            } }
        } .Else {
            .Default(System.Void)
        };
        .Label
        .LabelTarget CallSiteBinder.UpdateLabel:;
        .Label
            .If (
                .Call System.Runtime.CompilerServices.CallSiteOps.SetNotMatched($$site)
            ) {
                .Default(System.Object)
            } .Else {
                .Invoke (((System.Runtime.CompilerServices.CallSite`1[System.Func`3[System.Runtime.CompilerServices.CallSite,System.Object,System.Object]])$$site).Update)(
                    $$site,
                    $$arg0)
            }
        .LabelTarget #Label1:
    }
}

The cool thing is that the Target delegate of the callsite is set to point to the compiled version of this expression tree. Hence the next time we call target.Foo on an instance of MyObject1, we’ll dispatch really quickly into the TryGetMember code, and will continue to do so until we try to dispatch on something that isn’t of type MyObject1. On the way out, we add the compiled expression tree to the Rules associated with the CallSite. We can store at most 10 such rules, and we’ll see where they come in to play in a little while.

When we make a call on MyObject2, the code above misses the fast path and we go back in to UpdateAndexecute1. The code in this method looks at the rules that we have previously compiled and tries to find something that works. On this pass it doesn’t find anything, so we go back into the compile code and compile the following ExpressionTree.

.Lambda CallSite.Target<System.Func`3[System.Runtime.CompilerServices.CallSite,System.Object,System.Object]>(
    System.Runtime.CompilerServices.CallSite $$site,
    System.Object $$arg0) {
    .Block() {
        .If (
            $$arg0 .TypeEqual ConsoleApplication14.MyObject2
        ) {
            .Return #Label1 { .Block(System.Object $var1) {
                .If (
                    .Call ((System.Dynamic.DynamicObject)$$arg0).TryGetMember(
                        .Constant<System.Dynamic.GetMemberBinder>(Microsoft.CSharp.RuntimeBinder.CSharpGetMemberBinder),
                        $var1)
                ) {
                    $var1
                } .Else {
                    .Throw .New Microsoft.CSharp.RuntimeBinder.RuntimeBinderException("’ConsoleApplication14.MyObject2′ does not contain a definition for ‘Foo’")
                }
            } }
        } .Else {
            .Default(System.Void)
        };
        .Label
        .LabelTarget CallSiteBinder.UpdateLabel:;
        .Label
            .If (
                .Call System.Runtime.CompilerServices.CallSiteOps.SetNotMatched($$site)
            ) {
                .Default(System.Object)
            } .Else {
                .Invoke (((System.Runtime.CompilerServices.CallSite`1[System.Func`3[System.Runtime.CompilerServices.CallSite,System.Object,System.Object]])$$site).Update)(
                    $$site,
                    $$arg0)
            }
        .LabelTarget #Label1:
    }
}

This is the same as the first expression tree, apart from the change to the guard type. Again, this compiled delegate is set into the Target property of the CallSite so we’ll quickly dispatch to the right code for future calls to objects of type MyObject2 at this call site, and it is added to the rules for the callsite.

In our test program, we now dispatch to an instance of MyObject1. This causes the fast path to miss, and we get back into the UpdateAndexecute1 method.

The system gets the Rules for the CallSite, and in this case there are two that are applicable. One matches the MyObject1 case and the other matches the MyObject2 case. We pull the MyObject1 dispatcher from the rules cache, and set it into the Target property. We dispatch the call, notice it has been successful, and before we return the result, we call UpdateRules to move the rule that was successful earlier in the Rules array. This means that when new rules come into effect, it will take longer for it to be flushed out of the 10 item cache. We have also set the Target property to the matching rule, so we’ll dispatch to it quickly if we make another call on the same type.

The test program then goes on to make lots of calls on different types of target.

The Rules cache for the CallSite holds at most 10 items – roughly, the ten most recently used types at that call site. The BindCore method also maintains a cache at the level of the Binder for the call site, so that all call sites sharing the same binder can share the compiled results.

This is the cache that is used by the code that does:

      RuleCache<Func<CallSite, T0, TRet>> ruleCache = CallSiteOps.GetRuleCache<Func<CallSite, T0, TRet>>(site2);
      rules = ruleCache.GetRules();

This cache is also bounded, but can hold a lot more items which are more globally applicable. This cache is searched only after the site specific cache has been fully tried. This is a clever way to exploit the observation that for many programs, we tend to dispatch for large amounts of time on the same type at a given call site.

An easy way to see the optimised calling is to amend the code to:

    class MyBaseObject : DynamicObject
    {
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            System.Diagnostics.Debugger.Break();
            result = "Object1";
            return true;
        }
    }

and then run under windbg with the sos extension loaded. The first three calls are to a target of type MyObject1… during the first call the cache is set up, but the subsequent two calls don’t go through the setup code. The fourth call targets a different object type and therefore goes back through the cache setup code.

0:000> !ClrStack
OS Thread Id: 0x1b2c (0)
Child SP IP       Call Site
0042eecc 76ed22a1 [HelperMethodFrame: 0042eecc] System.Diagnostics.Debugger.BreakInternal()
0042ef24 62ee00e6 System.Diagnostics.Debugger.Break()
0042ef50 001f0777 ConsoleApplication14.MyBaseObject.TryGetMember(System.Dynamic.GetMemberBinder, System.Object ByRef)
0042ef68 0021017b DynamicClass.CallSite.Target(System.Runtime.CompilerServices.Closure, System.Runtime.CompilerServices.CallSite, System.Object)
0042ef88 60ac5ed3 System.Dynamic.UpdateDelegates.UpdateAndExecute1[[System.__Canon, mscorlib],[System.__Canon, mscorlib]](System.Runtime.CompilerServices.CallSite, System.__Canon)
0042efe0 001f0429 ConsoleApplication14.Program.Main(System.String[])
0042f2ac 634521db [GCFrame: 0042f2ac]

0:000> !ClrStack
OS Thread Id: 0x1b2c (0)
Child SP IP       Call Site
0042ef24 76ed22a1 [HelperMethodFrame: 0042ef24] System.Diagnostics.Debugger.BreakInternal()
0042ef7c 62ee00e6 System.Diagnostics.Debugger.Break()
0042efa8 001f0777 ConsoleApplication14.MyBaseObject.TryGetMember(System.Dynamic.GetMemberBinder, System.Object ByRef)
0042efc0 0021017b DynamicClass.CallSite.Target(System.Runtime.CompilerServices.Closure, System.Runtime.CompilerServices.CallSite, System.Object)
0042efe0 001f0429 ConsoleApplication14.Program.Main(System.String[])
0042f2ac 634521db [GCFrame: 0042f2ac]

0:000> !ClrStack
OS Thread Id: 0x1b2c (0)
Child SP IP       Call Site
0042ef24 76ed22a1 [HelperMethodFrame: 0042ef24] System.Diagnostics.Debugger.BreakInternal()
0042ef7c 62ee00e6 System.Diagnostics.Debugger.Break()
0042efa8 001f0777 ConsoleApplication14.MyBaseObject.TryGetMember(System.Dynamic.GetMemberBinder, System.Object ByRef)
0042efc0 0021017b DynamicClass.CallSite.Target(System.Runtime.CompilerServices.Closure, System.Runtime.CompilerServices.CallSite, System.Object)
0042efe0 001f0429 ConsoleApplication14.Program.Main(System.String[])
0042f2ac 634521db [GCFrame: 0042f2ac]

0:000> !ClrStack
OS Thread Id: 0x1b2c (0)
Child SP IP       Call Site
0042eeac 76ed22a1 [HelperMethodFrame: 0042eeac] System.Diagnostics.Debugger.BreakInternal()
0042ef04 62ee00e6 System.Diagnostics.Debugger.Break()
0042ef30 001f0777 ConsoleApplication14.MyBaseObject.TryGetMember(System.Dynamic.GetMemberBinder, System.Object ByRef)
0042ef48 0021030b DynamicClass.CallSite.Target(System.Runtime.CompilerServices.Closure, System.Runtime.CompilerServices.CallSite, System.Object)
0042ef68 60ac5ed3 System.Dynamic.UpdateDelegates.UpdateAndExecute1[[System.__Canon, mscorlib],[System.__Canon, mscorlib]](System.Runtime.CompilerServices.CallSite, System.__Canon)
0042efc0 002101db DynamicClass.CallSite.Target(System.Runtime.CompilerServices.Closure, System.Runtime.CompilerServices.CallSite, System.Object)
0042efe0 001f0429 ConsoleApplication14.Program.Main(System.String[])
0042f2ac 634521db [GCFrame: 0042f2ac]

The extended Expression trees allow us to express a lot of constructs from many source languages. One quick example would be a simple lambda expression which prints a message and then returns a value.

            BlockExpression res =
                Expression.Block(
                  Expression.Call(typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
                                  Expression.Constant("Hello")),
                  Expression.Constant(2));
            var delegateItem = Expression.Lambda(res).Compile();
            Console.WriteLine(delegateItem.DynamicInvoke());

The above code prints Hello and 2. The runtime contains a compiler for converting these expression trees into delegates making this an easy way to implement high level languages.

What I really started out wanting to investigate was the IDynamicMetaObjectProvider interface, but that will have to wait until next time.

Advertisements
This entry was posted in Computers and Internet. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s