📜 ⬆️ ⬇️

Plunge into the depths of C # dynamic

One of the most notable additions to C # 4 is dynamic. This has been told many times. But DLR (Dynamic Language Runtime) is always out of sight. In this article we will look at the internal structure of the DLR, the work of the compiler itself, and also give a definition of the concepts of a statically-dynamically-typed language with weak and strong typifications. And, of course, the PIC (Polymorphic Inline Cache) technique used in the Google V8 engine, for example, will not be overlooked.

Before moving on, I would like to refresh some of the terms and concepts.

In order not to repeat, by the expression variable we will mean any data object (variable, constant, expression).

Programming languages ​​by type checking criteria are usually divided into statically typed (the variable is associated with the type at the time of declaration and the type cannot be changed later) and dynamically typed (the variable is associated with the type at the time of assignment of the value and the type cannot be changed later).
')
C # is an example of a language with static typing, while Python and Ruby are dynamic.

According to the criterion of type security policies, languages ​​with weak (the variable does not have a strictly defined type) and strong / strict (the variable has a strictly defined type, which cannot be changed later) are typed.

C # 4 dynamic keyword

Although dynamic adds the ability to write clean code and interact with dynamic languages ​​like IronPython and IronRuby, C # does not cease to be a statically typed language with strong typing.

Before a detailed examination of the mechanism of the dynamic itself, we give an example of the code:

//    System.String dynamic d = "stringValue"; Console.WriteLine(d.GetType()); //       d = d + "otherString"; Console.WriteLine(d); //   System.Int32 d = 100; Console.WriteLine(d.GetType()); Console.WriteLine(d); //       d++; Console.WriteLine(d); d = "stringAgain"; //      d++; Console.WriteLine(d); 

The result of the execution is shown below in the screenshot:



And what do we see? What is the typing here?

I will answer immediately: typing is strong, and here's why.

Unlike other built-in types of the C # language (for example, string, int, object, etc.), dynamic has no direct mapping to any of the basic BCL types. Instead, dynamic is a special alias for System.Object with additional metadata necessary for proper late binding.

So, the view code:

 dynamic d = 100; d++; 

Will be converted to the form:

 object d = 100; object arg = d; if (Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee == null) { Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee = CallSite<Func<CallSite, object, object>>.Create(Binder.UnaryOperation(CSharpBinderFlags.None, ExpressionType.Increment, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) })); } d = Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee.Target(Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee, arg); 

As you can see, the variable d of type object is declared. Then binders from the Microsoft.CSharp library come into play.

DLR

For each dynamic expression in code, the compiler generates a separate dynamic node call , which is the operation itself.

So, for the view code

 dynamic d = 100; d++; 

a class like this will be generated:

 private static class <dynamicMethod>o__SiteContainerd { // Fields public static CallSite<Func<CallSite, object, object>> <>p__Sitee; } 

The field type <> p__Sitee is the class System.Runtime.CompilerServices.CallSite. Consider it in more detail.

 public sealed class CallSite<T> : CallSite where T : class { public T Target; public T Update { get; } public static CallSite<T> Create(CallSiteBinder binder); } 

Although the Target field is generic, in fact it is always a delegate . And the last line in the above example is not just a variation of the operation:

 d = Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee.Target(Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee, arg); 

The static Create method of the CallSite class is:

 public static CallSite<T> Create(CallSiteBinder binder) { if (!typeof(T).IsSubclassOf(typeof(MulticastDelegate))) { throw Error.TypeMustBeDerivedFromSystemDelegate(); } return new CallSite<T>(binder); } 

The Target field is an L0 cache (there are also L1 and L2 caches), which is used to quickly dispatch calls based on the call history.

I draw your attention to the fact that the call node is “self-learning”, so the DLR needs to periodically update the value of Target.

To describe the logic of the DLR I will give the answer of Eric Lippert on this (free translation):

First, the runtime decides what type of object we are dealing with (COM, POCO).

Next comes the compiler. Since there is no need for a lexer and a parser, DLR uses a special version of the C # compiler, which has only a metadata analyzer, a semantic expression analyzer, and a code generator, which instead of IL generates an Expression Trees.

The metadata analyzer uses reflection to determine the type of the object, which is then passed to the semantic analyzer to establish the possibility of calling a method or performing an operation. Next comes the construction of the Expression Tree, as if you were using a lambda expression.

The C # compiler brings the expression tree back to the DLR along with the caching policy. The DLR then stores this delegate in the cache associated with the call site.

To do this, use the Update property of the CallSite class. When you call a dynamic operation stored in the Target field, it is redirected to the Update property, where the call to the binders occurs. When the next time the call occurs, instead of re-performing the above actions, the already prepared delegate will be used.

Polymorphic Inline Cache

The performance of dynamic languages ​​suffers due to additional checks and search queries running in all places of the call. The rectilinear implementation is constantly looking for members in the class priority lists and, possibly, permits overloading of the method argument types each time a line of code is executed. In languages ​​with static typing (or with a sufficient number of type indications in the code and type inference), you can generate instructions or calls to runtime functions that are suitable for all call points. This is possible because static types let you know everything you need during compilation.

In practice, repeated operations on the same types of objects can be reduced to a general type. For example, when you first calculate the value of the expression x + y for integers x and y, you can remember a code snippet or an exact execution time function that adds two integers. Then, for all subsequent calculations, the value of this expression will not be required to search for a function or a code fragment due to the cache.

The above delegate caching mechanism (in this case) when the call node is self-learning and updated is called Polymorphic Inline Cache. Why?

Polymorphic . Target call node can have several forms, based on the types of objects used in a dynamic operation.

Inline . The life cycle of an instance of the CallSite class takes place exactly in the place of the call itself.

Cache Work is based on different cache levels (L0, L1, L3).

Source: https://habr.com/ru/post/144330/


All Articles