C# and Currying – Looking into Performance
In my previous post, I showed an example of how to use new C# features to simplify your code. In this post I’ll investigate that technique affects the performance of the code.
Let’s start from the beginning, there’s a function called f_original
static void f_original( int v1, string v2)
{
/* do something */
}
It takes two parameters. In this scenario I often call it with the first parameter always as 1. Like so…
f_original(1,"foo");
To avoid this repetition I want a way to consistently call with the first parameter being set to 1.
The first approach is to wrap the function.
static void f_with_wrapper(string v2)
{
f_original(1,v2);
}
And clearly we’ll call the wrapped function like this
f_with_wrapper( “foo” )
The second approach is use currying to create a new function
var f_with_currying = MAKE_FUNC(1);
where MAKE_FUNC is defined as …
static Action<string> MAKE_FUNC(int v1)
{
Action<string> new_func = (string v2) => f_original(v1, v2);
return new_func;
}
And we call this function like this
f_with_currying("foo");
NOTE: be aware of how often MAKE_FUNC is called. It takes time to create a function. So in the numbers I’m going to show two cases – one in which the curried function is created *every* time it is used, and another when it is statically created once and reused on each call.
Performance – the code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DemoCurrying2
{
class Program
{
static void Main(string[] args)
{
int n = 1000 * 1000 * 10;
Console.WriteLine("Number of repetitions: {0}",n);
time(demo_nothing, n, "demo_nothing");
time(demo_with_direct_call, n, "demo_with_direct_call");
time(demo_with_wrapper_function, n, "demo_with_wrapper_function");
time(demo_with_currying, n, "demo_with_currying");
time(demo_with_currying2, n, "demo_with_currying2");
}
static void time( Action f , int n,string name)
{
System.DateTime start = System.DateTime.Now;
for (int i = 0; i < n; i++)
{
f();
}
System.DateTime end = System.DateTime.Now;
var duration = end - start;
Console.WriteLine("{0} \t Duration \t {1}",name,duration.TotalSeconds);
}
static void demo_nothing()
{
}
static void demo_with_direct_call()
{
f_original(1,"foo");
}
static void demo_with_wrapper_function()
{
f_with_wrapper("foo");
}
static void demo_with_currying()
{
var f_with_currying = MAKE_FUNC(1);
f_with_currying("foo");
}
static Action<string> pre_defined_f_with_currying = MAKE_FUNC(1);
static void demo_with_currying2()
{
pre_defined_f_with_currying("foo");
}
static void f_original( int v1, string v2)
{
/*var sb = new System.Text.StringBuilder();
sb.Append("Hello World: ");
sb.AppendFormat(" v1={0} ", v1);
sb.AppendFormat(" v2={0} ", v2);
string s = sb.ToString();*/
}
static void f_with_wrapper(string v2)
{
f_original(1,v2);
}
static Action<string> MAKE_FUNC(int v1)
{
Action<string> new_func = (string v2) => f_original(v1, v2);
return new_func;
}
}
}
Performance – the numbers (part one)
- The test loop 10 million times for each “demo” method
- In this case we left f_original doing nothing so that we get a better sense of the base overhead.
- demo_with_currying – this is the one that curries the function each time.
- demo_with_currying – this one caches the curried function
- demo_nothing – so that we can measure the influence of the performance testing code itself
The times
What to note:
- We can see the impact of currying on each call to demo_with_currying
- demo_with_currying2 and demo_with_wrapper have very similar execution times
- demo_with_direct is only slightly faster than demo_with_currying2 and demo_with_wrapper
Performance – the numbers (part two)
- In this case we left f_original I will make f_original do something small …
static void f_original( int v1, string v2)
{
var sb = new System.Text.StringBuilder();
sb.Append("Hello World: ");
sb.AppendFormat(" v1={0} ", v1);
sb.AppendFormat(" v2={0} ", v2);
string s = sb.ToString();
}
The times
What to note:
- Now that f_original is doing some real work – we can see that the differences between the methods are minor
- demo_with_direct, demo_with_currying2, demo_with_wrapper – they had roughly the same execution time
- demo_with_currying and demo_with_currying2 – the impact of currying every on every loop had a slight impact
- demo_nothing is small
Parting Thoughts
- Caveat Emptor – this isn’t meant to be an exhaustive evaluation of performance – just a quick sanity check to ensure. As always with any performance measurement it will depend on the specific usage scenarios and numbers. When in doubt: measure.