While working on a cusom intepreter of our DSL written purely in c# I am trying to understand how to correctly take advantage of the built in call site caching feature. Here is my little demonstration program that works only until the types change.
using System.Collections.ObjectModel;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
public class Program2
{
public static void Main()
{
// Create a CallSite
var callSite = CallSite<Func<CallSite, object, object, object>>.Create(new AdditionBinder());
// Perform dynamic addition
var add = callSite.Target;
var result1 = add(callSite, 5, 10); // Bind for int,int gets called for the first time and compiled fn(int,int) is created
var result2 = add(callSite, 10, 10); // Bind not called, the compiled fn(int,int) is reused. Nice!
var result3 = add(callSite, "strA", "str1"); // before Bind was even called I got exception "Unable to cast object of type 'System.String' to type 'System.Int32" :-(
var result4 = add(callSite, "strB", "str2");
}
}
Here is how the custom binder is implemented. The logic how to switch over the types is simplistic and will be better after I make sure that this code gets called correctly just once per each combination of types. There is nothing else in the whole csproj.
public class AdditionBinder : CallSiteBinder
{
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
{
// Get the two arguments that we're adding
var left = parameters[0];
var right = parameters[1];
// Here comes later logic how to prepare for tailormade type specific compiled variants
Expression addition = null;
if (args[0] is int && args[1] is int)
{
// Perform int addition
addition = Expression.Convert( Expression.Add(
Expression.Convert(left, typeof(int)),
Expression.Convert(right, typeof(int))
), typeof(object)
);
}
else if (args[0] is string && args[1] is string)
{
// Perform string addition
addition = Expression.Convert(
Expression.Add(
Expression.Convert(left, typeof(string)),
Expression.Convert(right, typeof(string)),
typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) })
), typeof(object)
);
}
else
throw new InvalidOperationException("No add for T + T");
// Return the result of the addition
return Expression.Return(returnLabel, addition);
}
}
I have debugged into the CallSiteBinder.cs but I was not clear I undersand it and that I have corretly downloaded all necessary parts.
Do I use the Binder correctly? Do I use the target or call site correctly? I feel like I am really close. I want to post this here, because I have never found a complete example online. I hope we figure it out and this page helps others. Thanks for your kind advice.
Update
I tried two distinct instaces of CallSite that share binder and got this:
var sharedBinder = new AdditionBinder();
var callSite = CallSite<Func<CallSite, object, object, object>>.Create(sharedBinder);
var callSite2 = CallSite<Func<CallSite, object, object, object>>.Create(sharedBinder);
_ = callSite2.Target(callSite2, "str", "str"); // works
_ = callSite.Target(callSite, 5, 10); // throws as if the callsite instances shared the cached Target
Example of the interpreted DSL
# I assume one instance of CallSiteBinder has cache with all specialized
# specific typed compiled delegates, for myAdd definition
myAdd = x,y => x+y
# adds 42 to all integers
# I assume one instance of 'CallSiteCache' for myAdd call here, that typically ends up with L1 cached myAdd(int,int)
# but does not thow in possible string and uses the shared CallSiteBinder with some L2 cache of delegates
exampleAlgoInts = intArr => intArr.Select(i => myAdd(i,42))
# adds 'Hi ' to all names
# I assume another instance of 'CallSiteCache' for myAdd call here, that typically ends up with L1 cached myAdd(string,string)
# but does not thow in possible double, int and uses the shared CallSiteBinder with some L2 cache of delegates
exampleAlgoStrings = strArr => strArr.Select(name => myAdd("Hi ",name))
I know that I am using internals that may not be intended to be used by developers outside of the core team. But I still think it is interesting to know how this works and might help performance for many other interpreter-writers.
Handling the dynamic expressions inside the overridden
Bind
method is causing the problem, and here is a solution that will allow you to use the sameCallSite
for both calls dynamically:The only difference is that here we are using a dynamic object and defining two different delegates on that object that will handle the functionality for both operations for our shared binder (
AddInts
andConcatStrings
). We are also declaring and passing aBinder
usingInvokeMember
and not overriding to create our ownCallSiteBinder
. We can do this, but theBind
method is responsible for generating the expression of the passed operation. We would want to handle the logic for different parameters/return type in separate expression definitions.The binder has a set of restrictions, that will evaluate to true or false, and this determines if the cached result can be reused. If there are different parameter types, then the cached result cannot be reused, and a new expression will be generated by the
CallSiteBinder
.More on restrictions here: https://www.red-gate.com/simple-talk/blogs/inside-the-dlr-invoking-methods/
And another useful post on DLR and how the binder handles restrictions: https://stackoverflow.com/a/4653954/17290059