Friday, June 04, 2010 12:19 PM
by
RickM
A Speedy Alternative to F# DelegateEvents
DelegateEvents use DynamicInvoke under the hood and so can be slow when trigged frequently. As we have an existing model which requires an event interface, this was was causing our F# implementation to be a bit slower than the C# we were comparing it to. Thankfully, F# allows you to implement your own eventing via IDelegateEvent. With a standard Invoke version of DelegateEvent provided by James Margetson, our implementation in F# is running at equivalent speed to C#.
The other day I was working with Steve on trying to discover why a particular segment of apparently equivalent code was running ~25% slower in F# when compared to C#. We broke out AQTime and did a line by line release-mode profile. The frequent triggering of a DelegateEvent immediately jumped out as the cause.
Needless to say, I was a bit concerned. What could be causing this slow eventing in my favorite language? After a brief review of the generated IL, nothing in particular seemed fishy. So, I quickly built a sample and sent it off to my favorite language team.
The contents of the F# library assembly:
type FsEventClass(num) =
let event = new DelegateEvent<System.EventHandler<System.EventArgs>>()
[<CLIEvent>]
member this.Event = event.Publish
member this.Run () =
let args = [| this :> obj ; System.EventArgs.Empty :> obj |]
for i in 1 .. num do
args.[1] <- new System.EventArgs() :> obj
event.Trigger( args )
And this, the C# client:
static void Main(string[] args)
{
int iters = 1000000;
DateTime fsStart = DateTime.Now;
FSharpEventingLib.FsEventClass fs = new FSharpEventingLib.FsEventClass(iters);
int fsCalled = 0;
fs.Event += (s, a) => fsCalled++;
fs.Run();
DateTime fsEnd = DateTime.Now;
TimeSpan fsTime = fsEnd - fsStart;
System.Console.WriteLine(String.Format("F# took: {0} when called {1} times", fsTime, fsCalled));
}
Finally, the program output:
F# took: 00:00:05.6830000 when called 1000000 times
As it turns out, the F#’s current DelegateEvent uses a DynamicInvoke under the hood and that can slow things down quite a bit. This won’t be important in most cases as Event is implemented with a standard Invoke. However, in our case we had to fit to an existing C# model and so needed to do an event trigger for each resulting scanline of an image.
A big thanks to James Margetson of the F# Team for the following fast replacement for DelegateEvent. Within same day I had mentioned my issue he had this solution for me.
The new library contents:
type FastDelegateEvent() =
let mutable multicast : System.EventHandler = null
member x.Trigger(sender:obj,args:System.EventArgs) =
match multicast with
| null -> ()
| d -> d.Invoke(sender,args) // DelegateEvent used: d.DynamicInvoke(args) |> ignore
member x.Publish =
{ new IDelegateEvent<System.EventHandler> with
member x.AddHandler(d) =
multicast <- System.Delegate.Combine(multicast, d) :?> System.EventHandler
member x.RemoveHandler(d) =
multicast <- System.Delegate.Remove(multicast, d) :?> System.EventHandler }
type FsFastEventClass(num) =
let event = new FastDelegateEvent()
[<CLIEvent>]
member this.Event = event.Publish
member this.Run () =
for i in 1 .. num do
event.Trigger(this, new System.EventArgs())
The program output with FastDelegateEvent:
F# took: 00:00:00.0390000 when called 1000000 times
This new event class completely resolved our issue. With it our F# version is just as fast, if not faster, than the C# equivalent.