Visitor Versus 'is' Cast in C#
Obviously C# is not C++, and so it is important to not simply assume that things true in C++ are also true in C#. A good question is the cost of the is operator compared to a virtual (or abstract) function. Is a visitor better than casting? This is pretty easy to test, but I couldn’t find any good post about this, so I wrote it myself.
A few things should affect the result. Most importantly, the speed of the cast approach depends on the number of if
tests you have to do, or equivalently, the number of failed tests. The more classes you have to test, the slower things should be. The result might also depend on class hierarchy depth. To test this out, I designed the following class hierarchy.
I then wrote to code to either cast via is
or call a visitor function. Everything except the Circle
class can be entirely removed from the code by conditional compiles. This way, I can test the effects of adding more classes or increasing the class hierarchy depth and this is detailed in the table below.
Classes | Visitor Time (ms) | Visitor Average | Cast Time (ms) | Cast Average | Fastest |
---|---|---|---|---|---|
Circle |
1218 |
0.12 |
798 |
0.08 |
Cast |
Circle Square |
2259 |
0.12 |
1890 |
0.09 |
Cast |
Circle |
3339 |
0.11 |
3769 |
0.13 |
Visitor |
Circle |
3286 |
0.11 |
3693 |
0.12 |
Visitor |
Circle |
4501 |
0.11 |
5703 |
0.14 |
Visitor |
Circle |
5541 |
0.11 |
7920 |
0.16 |
Visitor |
Times increase with each addition because the total number of objects increases, so the important number is the average. The loop is also run 10000 times so that the times are much larger than the timer resolution of 1 ms.
Overall, the results are not that unexpected. As you add more types, the number of failed if’s increases, and the performance degrades. The visitor stays the same. Interestingly, at the lowest end, the cast is faster than the visitor, and so for simple checks, the better choice might be the cast.
You can see the full source below.
// Comment the conditional compile lines below to
// remove particular types from the object hierarchy
#define SQUARE
#define ROUNDEDSQUARE
#define HEXAGON
#define OCTAGON
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace VisitorVsCast
{
/// <summary>
/// The visitor interface
/// </summary>
public interface IVisitor
{
void VisitCircle(Circle circle);
#if SQUARE
void VisitSquare(Square square);
#endif
#if ROUNDEDSQUARE
void VisitRoundedSquare(RoundedSquare square);
#endif
#if HEXAGON
void VisitHexagon(Hexagon hexagon);
#endif
#if OCTAGON
void VisitOctagon(Octagon octagon);
#endif
}
/// <summary>
/// Base class for all shapes
/// </summary>
public abstract class Shape
{
public abstract void AcceptVisitor(IVisitor visitor);
}
/// <summary>
/// A circle
/// </summary>
public class Circle : Shape
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitCircle(this);
}
}
#if SQUARE
/// <summary>
/// A square shape
/// </summary>
public class Square : Shape
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitSquare(this);
}
}
#endif
#if ROUNDEDSQUARE
/// <summary>
/// A rounded square (a square with round corners)
/// </summary>
public class RoundedSquare : Square
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitRoundedSquare(this);
}
}
#endif
#if HEXAGON
public class Hexagon : Shape
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitHexagon(this);
}
}
#endif
#if OCTAGON
public class Octagon : Shape
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitOctagon(this);
}
}
#endif
/// <summary>
/// Counting visitor
/// </summary>
public class CountingVisitor : IVisitor
{
public void VisitCircle(Circle circle)
{
}
#if SQUARE
public void VisitSquare(Square square)
{
}
#endif
#if ROUNDEDSQUARE
public void VisitRoundedSquare(RoundedSquare square)
{
}
#endif
#if HEXAGON
public void VisitHexagon(Hexagon hexagon)
{
}
#endif
#if OCTAGON
public void VisitOctagon(Octagon octagon)
{
}
#endif
}
class Program
{
static int NumObjectsPerCategory = 10000;
static int NumIterations = 10000;
static void Main(string[] args)
{
long visitorTime = 0;
long castTime = 0;
// Create a list with lots of shapes. It doesn't matter the order
// of the shapes, but that we have a bunch
List<Shape> shapes = new List<Shape>();
shapes.Capacity += 5 * NumObjectsPerCategory;
for (int i = 0; i < NumObjectsPerCategory; ++i)
{
shapes.Add(new Circle());
}
#if SQUARE
for (int i = 0; i < NumObjectsPerCategory; ++i)
{
shapes.Add(new Square());
}
#endif
#if ROUNDEDSQUARE
for (int i = 0; i < NumObjectsPerCategory; ++i)
{
shapes.Add(new RoundedSquare());
}
#endif
#if HEXAGON
for (int i = 0; i < NumObjectsPerCategory; ++i)
{
shapes.Add(new Hexagon());
}
#endif
#if OCTAGON
for (int i = 0; i < 10000; ++i)
{
shapes.Add(new Octagon());
}
#endif
visitorTime = CountByVisitor(shapes);
castTime = CountByCast(shapes);
Console.WriteLine("Visitor: {0}", visitorTime);
Console.WriteLine("Cast: {0}", castTime);
}
/// <summary>
/// Iterate them by the visitor
/// </summary>
/// <param name="shapes"></param>
/// <returns></returns>
static long CountByVisitor(List<Shape> shapes)
{
CountingVisitor visitor = new CountingVisitor();
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < NumIterations; ++i)
{
foreach (Shape shape in shapes)
{
shape.AcceptVisitor(visitor);
}
}
return sw.ElapsedMilliseconds;
}
/// <summary>
/// Iterate them by cast
/// </summary>
/// <param name="shapes"></param>
/// <returns></returns>
static long CountByCast(List<Shape> shapes)
{
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < NumIterations; ++i)
{
foreach (Shape shape in shapes)
{
if (shape is Circle)
{
}
#if ROUNDEDSQUARE
else if (shape is RoundedSquare)
{
}
#endif
#if SQUARE
else if (shape is Square)
{
}
#endif
#if HEXAGON
else if (shape is Hexagon)
{
}
#endif
#if OCTAGON
else if (shape is Octagon)
{
}
#endif
}
}
return sw.ElapsedMilliseconds;
}
}
}