Sunday, August 21, 2016

Dynamically Invoke Methods Quickly, with InvokeHelpers.EfficientInvoke

In my previous blog post, I talked about Optimizing Dynamic Method Invokes in .NET. In this post, we will use that information to create a static helper method that is twice as fast as MethodInfo.Invoke.

Basically, we create and cache a delegate in a concurrent dictionary, and then cast both it and it's arguments to dynamics and invoke them directly. The concurrent dictionary introduces overhead, but it still more than twice as fast as calling MethodInfo.Invoke. Please note that this method is highly optimized to reduce the use of hash code look ups, property getters, closure allocations, and if checks.

let's take a look at the code...

InvokeHelpers.EfficientInvoke

public static class InvokeHelpers
{
    private const string TooManyArgsMessage = "Invokes for more than 10 args are not yet implemented";
 
    private static readonly Type VoidType = typeof(void);
 
    private static readonly ConcurrentDictionary<Tuple<string, object>, DelegatePair> DelegateMap 
        = new ConcurrentDictionary<Tuple<string, object>, DelegatePair>();
 
    public static object EfficientInvoke(object obj, string methodName, params object[] args)
    {
        var key = Tuple.Create(methodName, obj);
        var delPair = DelegateMap.GetOrAdd(key, CreateDelegate);
            
        if (delPair.HasReturnValue)
        {
            switch (delPair.ArgumentCount)
            {
                case 0: return delPair.Delegate();
                case 1: return delPair.Delegate((dynamic)args[0]);
                case 2: return delPair.Delegate((dynamic)args[0], (dynamic)args[1]);
                case 3: return delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2]);
                case 4: return delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3]);
                case 5: return delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4]);
                case 6: return delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5]);
                case 7: return delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5], (dynamic)args[6]);
                case 8: return delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5], (dynamic)args[6], (dynamic)args[7]);
                case 9: return delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5], (dynamic)args[6], (dynamic)args[7], (dynamic)args[8]);
                case 10: return delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5], (dynamic)args[6], (dynamic)args[7], (dynamic)args[8], (dynamic)args[9]);
                default: throw new NotImplementedException(TooManyArgsMessage);
            }
        }
 
        switch (delPair.ArgumentCount)
        {
            case 0: delPair.Delegate(); break;
            case 1: delPair.Delegate((dynamic)args[0]); break;
            case 2: delPair.Delegate((dynamic)args[0], (dynamic)args[1]); break;
            case 3: delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2]); break;
            case 4: delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3]); break;
            case 5: delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4]); break;
            case 6: delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5]); break;
            case 7: delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5], (dynamic)args[6]); break;
            case 8: delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5], (dynamic)args[6], (dynamic)args[7]); break;
            case 9: delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5], (dynamic)args[6], (dynamic)args[7], (dynamic)args[8]); break;
            case 10: delPair.Delegate((dynamic)args[0], (dynamic)args[1], (dynamic)args[2], (dynamic)args[3], (dynamic)args[4], (dynamic)args[5], (dynamic)args[6], (dynamic)args[7], (dynamic)args[8], (dynamic)args[9]); break;
            default: throw new NotImplementedException(TooManyArgsMessage);
        }
 
        return null;
    }
 
    private static DelegatePair CreateDelegate(Tuple<string, object> key)
    {
        var method = key.Item2
            .GetType()
            .GetMethod(key.Item1);
 
        var argTypes = method
            .GetParameters()
            .Select(p => p.ParameterType)
            .Concat(new[] { method.ReturnType })
            .ToArray();
 
        var newDelType = Expression.GetDelegateType(argTypes);
        var newDel = Delegate.CreateDelegate(newDelType, key.Item2, method);
 
        return new DelegatePair(newDel, argTypes.Length - 1, method.ReturnType != VoidType);
    }
 
    private class DelegatePair
    {
        public DelegatePair(dynamic del, int argumentCount, bool hasReturnValue)
        {
            Delegate = del;
            ArgumentCount = argumentCount;
            HasReturnValue = hasReturnValue;
        }
 
        public readonly dynamic Delegate;
        public readonly int ArgumentCount;
        public readonly bool HasReturnValue;
    }
}

Now let's take a look at some performance tests...

Optimizing Dynamic Method Invokes in .NET

I recently had a lot of fun helping to optimize some RPC code that was using reflection to dynamically invoke methods in a C# application. Below are a list of implementations that we experimented with, and their performance.

  1. Directly Invoking the Method
  2. Using MethodInfo.Invoke
  3. Using Delegate.DynamicInvoke
  4. Casting to a Func
  5. Casting a Delegate to Dynamic

Spoilers: Here are the results. (The tests for this can be see below.)

Name First Call (Ticks) Next Million Calls Invoke Comparison
Invoke 1 39795 -
MethodInfo.Invoke 12 967523 x24
Delegate.DynamicInvoke 32 1580086 x39
Func Invoke 731 41331 x1
Dynamic Cast 1126896 85495 x2

Conclusion: Invoking a method or delegate directly is always fastest, but when you need to execute code dynamically, then (after the first invoke) the dynamic invoke of a delegate is significantly faster than using reflection.

Let's take a look at the test code...

Sunday, July 31, 2016

Can I port my application to .NET Core?

I am very excited about .NET Core, and I am looking forward to porting my projects to it. Generally porting your own code is a straight forward task, but updating all of your dependencies can be quite daunting. Good news: the guys over at Octopus have built an awesome little site that helps you determine the state of your dependencies!

I Can Has .NET Core

Enjoy,
Tom

Sunday, July 24, 2016

Make your NuGet Server use NLog

The latest version of NuGet.Server is fast, stable, and super simple to setup. As for most .NET tools, Scott Hanselman already create a great write up about how to use it.

However, I was very disappointed at how unintuitive it was to get wire up a custom logger!

You need to take several steps to make the official NuGet server write to something like NLog:

  1. Create a log wrapper.
  2. Implement NuGet.Server.Logging.ILogger.
  3. Implement NuGet.ILogger...
  4. ...which also makes you implement NuGet.IFileConflictResolver!
  5. Implement your own NuGet.Server.IServiceResolver
  6. When instantiating ServerPackageRepository...
  7. ...pass in the ILogger...
  8. ...AND set the Logger property!

Still confused? Pull the code here, or take a look below!

Sunday, July 17, 2016

Should you open source your software?

Every Tuesday I help host the QQ Cast, where we fabricate answers to geek culture's most superfluous questions. For our 50th Quest, my good friend Matt Stevenson joined us to talk about open source software.

QQ Cast - Quest 50 - Should you open source your software?

  • 00:00 - Mic Check and Introductions
  • 05:30 - Is "Expand Enhance Extinguish" still alive and well?
  • 19:40 - Can we build a sustainable infrastructure without open source software?
  • 33:00 - How much structure do we like to see in our frameworks?
  • 40:50 - What are some great open source projects?
  • 47:15 - Was heartbleed a good thing or a bad thing?
  • 54:30 - Does contributing to open source projects help your resume?
  • 60:00 - Should you open source your software?
  • 70:00 - Wrap up!

Please always remember that all views and opinions expressed on the podcast are representative solely of the person expressing them; not of their friends and family, not of their coworkers, and certainly not of their employers, past, present, or future.

Enjoy,
Tom

Thursday, June 30, 2016

Updating Default Parameters in .NET Assemblies

One of the things that I love about C# is how so many of it's features are just very conveniently designed compiler tricks. This means that, just like any other magic trick, once you know how the trick is performed you immediately realize that there really is nothing magical about it.

So, let's talk about default parameters. They are actually just constant values that get compiled into your code when you go to use a method that has them. Let's look at an example...

The Code

public enum Country
{
    US,
    CA
}
 
public static class SharedUtility
{
    public static bool CanDrink(
        int age, 
        Country country = Country.CA)
    {
        switch (country)
        {
            case Country.US:
                return age >= 21;
 
            case Country.CA:
                return age >= 18;
 
            default:
                throw new ArgumentException(
                    "Invalid Country",
                    nameof(country));
        }
    }
}
 
public class SharedUtilityTests
{
    [Fact]
    public void CanDrink()
    {
        var result = SharedUtility.CanDrink(20);
 
        // The line above will compile into the following:
        // var result = SharedUtility.CanDrink(20, Country.CA);
        // Thus, the Assert below will succeed!
 
        Assert.True(result, "The default was not US!");
    }
}

So, now that you know how the trick is performed, how could this cause a problem for you? Specifically, what happens if you update a library that exposes methods with default parameters?

Nothing will change, until you recompile against the library!

If another library changes their default parameters, but you do not recompile your code against it, then your code will be using the old default values! Let's look back at the previous example and see why this could cause confusion...

Sunday, June 26, 2016

MP3 Playlist for Chromecast

Come to find out, I am a very retro guy when it comes to music. I have these old things that I like to use to play music, you may not have heard of them, they are called MP3s. They are kind of like 8-track tapes, but digital, not in the cloud, and not old enough to be cool yet.

How do you play MP3s via Chromecast?

The official answer is to use Google Play, but that implies that you want to both pay for that service and upload your files to the cloud. The unofficial answer is to drag and drop your MP3s into a Chrome tab and cast that tab, however this does not allow you to create a playlist.

Introducing Playlist for Chromecast

I have created a simple single page HTML 5 application that will act as a playlist for MP3s on your computer. Just download the project and open up release/playlist.html in Chrome, then drag and drop MP3s on to the page.

Development

I had a lot of fun making this, and I'm not done. I intend to use this project as a case study to talk about VSCode, TypeScript, SASS, HTML5 Audio, NPM, and unit testing. For now, I just wanted to start by getting this initial post up, but expect more to follow.

What's next?

  • Update project documentation.
  • Create unit tests.
  • Add theme support.
  • Write blog posts about development.
  • Maybe host it on a domain.
  • Maybe submit it as a Chrome application.

Enjoy,
Tom

Real Time Web Analytics