Often in the newsgroups, people ask the fastest way of doing something. The first thing I believe it's important to realise is that unless you know that that task is causing a bottleneck in your real application, it's probably not worth working out what the truly fastest way of doing it is. Instead, write the clearest code which does the job - it's much better to take a while to get the right answer than to get the wrong answer very quickly.
Having said that, I've always enjoyed a bit of microbenchmarking. For proper application-wide benchmarking, you should be using a decent profiler etc, worrying about all kinds of different aspects of performance. A server-side app, for instance, should worry about latency as well as total throughput, and of course scalability and reliability. This page doesn't attempt to cover any of that.
This page provides a very simple microbenchmarking framework which I now use when testing different ways of attacking a problem. It's rough and ready, but it does well enough for newsgroup posts and testing for fun.
The code has grown slightly since its first incarnation, and it no longer makes sense to have it inline in this page. However, it's still just a single file to download: Benchmark.cs. Once you've downloaded it, using it is simple: just compile it into the same assembly as the code you wish to benchmark, and run it. It will find all the types with benchmarking code in, and run that code, reporting the timings. It's probably best to demonstrate this with a small sample benchmark:
using System; using System.Collections; public class FillingArrayList { [Benchmark] public static void Test() { ArrayList al = new ArrayList(); for (int i=0; i < 10000000; i++) al.Add ("hello"); } } |
The only "special" thing about this class is that the Test
method is marked with the Benchmark
attribute (which is
a simple attribute which can only be applied to methods - it's declared
in Benchmark.cs
). The method must be public, static,
and not take any parameters, otherwise the framework ignores it. All
the methods which obey these rules are executed and timed (one at a time).
I almost always compile simple test programs from the command line -
if you wish to use Visual Studio, it's just a case of having
Benchmark.cs
in the project. If you have other classes
which have a Main
method, set Benchmark
as
the startup class. The project should be set to be a console application
project. When compiling from the command line I simply use
something like: cs Benchmark.cs FillingArrayList.cs
(using
the above code as an example). This produces Benchmark.exe
which I can then run directly.
Running the code produces results like this:
Benchmarking type FillingArrayList Test 00:00:02.1044640 |
Hopefully the meaning is self-explanatory!
Some of the time, a test like the above is all that's needed. Often you'll want a bit more though. The framework allows you to use command-line parameters to control the tests, along with ways of resetting the class between tests and checking the results after each test. Here's an example showing all of these features, as well as demonstrating what happens when you have more than one benchmark method in a class. Note that you can use any of these features individually, or indeed none of them at all.
// See https://jonskeet.uk/csharp/benchmark.html for how to run this code using System; using System.Collections; public class FillingArrayList { static int iterations=10000000; // Give it a reasonable default static ArrayList list; // This is what we'll check public static void Init(string[] args) { if (args.Length>0) iterations = Int32.Parse(args[0]); } public static void Reset() { // Make sure that tests which don't produce // anything don't end up using the previous result list = null; } public static void Check() { if (list.Count != iterations) throw new Exception ("List doesn't have the right size."); } [Benchmark] public static void Simple() { int count=iterations; ArrayList al = new ArrayList(); for (int i=0; i < count; i++) al.Add ("hello"); // Set the result list=al; } [Benchmark] public static void RightSizing() { int count=iterations; ArrayList al = new ArrayList(count); for (int i=0; i < count; i++) al.Add ("hello"); // Set the result list=al; } } |
The first thing to notice is that the number of iterations is
no longer hardcoded. There's a static variable, iterations
,
which the tests use to determine how many iterations to run for. This
has a default value (which is the same as the hardcoded value we used before),
but it can also be set in the new Init
method. This method
must be public and static, and take an array of strings as its only parameter.
The value passed in will never be null.
Before each test is run, the Reset
method is called (if it is
present). Like benchmark methods, it must be public, static and parameterless
to be noticed by the framework. This can be used to clear any information
which might be left from the previous test, for instance. In this example,
we make sure that the list
variable is null before the start of
the test.
After each test is run, the Check
method is called (if it
is present). Again, it must be public, static and parameterless. In practice,
you can often get away with resetting state at the end of this method rather
than having a separate Reset
method, but you may prefer to use
Reset
for clarity. The aim of the Check
method is
to verify that the results of the test are correct, and throw an exception
if they're not. If the check fails, the message of the exception is printed
out instead of the time taken. In this example, we check that the size of
the resultant list is correct. Note that this check is carried out after
the time has been calculated, so you can afford to put time-consuming checks
in here without affecting the results.
Here are the results of two runs of the new class - once with a parameter on the command line, and once without.
c:\test>benchmark 1000000 Benchmarking type FillingArrayList Simple 00:00:00.1001440 RightSizing 00:00:00.0801152 c:\test>benchmark Benchmarking type FillingArrayList Simple 00:00:02.1731248 RightSizing 00:00:00.7610944 |
There are now two command-line options available, which must come before any
parameters you wish to be passed to the benchmarked types themselves. The types
will only receive the options from the point of the first option which isn't
understood, or after the pseudo-option -endoptions
. (This last allows
you to write tasks which use the same option names as the benchmark framework
understands, if you really want to.) The options are:
-version
-runtwice
That's pretty much all there is to it. If you feel that there's a feature missing, please mail me at skeet@pobox.com to let me know. I suppose the most obvious "feature" to add would be a GUI interface instead of the current command-line one, but that will have to come at a later date, if at all.
A few guidelines when it comes to writing benchmarks which will give some useful information:
Check
method. This will only work while adding
to the total doesn't take long compared with the time taken to
calculate what I need to add though.
Just in case you want to see the code but don't actually want to download it separately, here's the full code for the framework.
using System; using System.Reflection; using System.Collections; /// <summary> /// The attribute to use to mark methods as being /// the targets of benchmarking. /// </summary> [AttributeUsage(AttributeTargets.Method)] public class BenchmarkAttribute : Attribute { } /// <summary> /// Very simple benchmarking framework. Looks for all types /// in the current assembly which have static parameterless /// methods public class Benchmark { public static void Main(string[] args) { // Save all the benchmark classes from doing a nullity test if (args==null) args = new string[0]; // We're only ever interested in public static methods. This variable // just makes it easier to read the code... BindingFlags publicStatic = BindingFlags.Public | BindingFlags.Static; foreach (Type type in Assembly.GetCallingAssembly().GetTypes()) { // Find an Init method taking string[], if any MethodInfo initMethod=type.GetMethod ("Init", publicStatic, null, new Type[]{typeof(string[])}, null); // Find a parameterless Reset method, if any MethodInfo resetMethod=type.GetMethod ("Reset", publicStatic, null, new Type[0], null); // Find a parameterless Check method, if any MethodInfo checkMethod=type.GetMethod("Check", publicStatic, null, new Type[0], null); // Find all parameterless methods with the [Benchmark] attribute ArrayList benchmarkMethods=new ArrayList(); foreach (MethodInfo method in type.GetMethods(publicStatic)) { ParameterInfo[] parameters = method.GetParameters(); if (parameters!=null && parameters.Length != 0) continue; if (method.GetCustomAttributes (typeof(BenchmarkAttribute), false).Length != 0) { benchmarkMethods.Add (method); } } // Ignore types with no appropriate methods to benchmark if (benchmarkMethods.Count==0) continue; Console.WriteLine ("Benchmarking type {0}", type.Name); // If we've got an Init method, call it once try { if (initMethod!=null) initMethod.Invoke (null, new object[]{args}); } catch (TargetInvocationException e) { Exception inner = e.InnerException; string message = (inner==null ? null : inner.Message); if (message==null) message = "(No message)"; Console.WriteLine ("Init failed ({0})", message); continue; // Next type } foreach (MethodInfo method in benchmarkMethods) { try { // Reset (if appropriate) if (resetMethod!=null) resetMethod.Invoke(null, null); // Give the test as good a chance as possible // of avoiding garbage collection GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); // Now run the test itself DateTime start = DateTime.Now; method.Invoke (null, null); DateTime end = DateTime.Now; // Check the results (if appropriate) // Note that this doesn't affect the timing if (checkMethod!=null) checkMethod.Invoke(null, null); // If everything's worked, report the time taken, // nicely lined up (assuming no very long method names!) Console.WriteLine (" {0,-20} {1}", method.Name, end-start); } catch (TargetInvocationException e) { Exception inner = e.InnerException; string message = (inner==null ? null : inner.Message); if (message==null) message = "(No message)"; Console.WriteLine (" {0}: Failed ({1})", method.Name, message); } } } } } |
Back to the main page.