Type strings vs Reflection in C#

Posted in software by Christopher R. Wirz on Fri Feb 03 2017

We previously found that compiler services provide an efficient way of determining an object's type in a PlugIn architecture. However, types that are not part of the default implementation (e.g., from third party plugins) still need reflection to get type information. Is it better to only use reflection for simplicity? Or should we still check for our whitelisted types?

If so, how should we do it?

  • item is SomeType
  • switch (item){ case SomeType ...
  • item.GetType() == typeof(SomeType)
  • item.GetType().Name == nameof(SomeType)
  • item.Type == nameof(SomeType) where .Type is a string property

Let's try a few approaches to string equality that will demonstrate the differences:


/// <summary>
/// Runs a test given a number of items
/// </summary>
/// <param name="numberOfItems">The number of items</param>
public static Dictionary<string, long> Run(int numberOfItems)
{
	var ret = new Dictionary<string, long>();

	IEnumerable<BaseType> collection = Enumerable.Range(1, numberOfItems)
		.Select(i => i % 2 == 0 ? new SomeType() as BaseType : new SomeOtherType())
		.ToArray();

	var current = new BaseType();
	foreach (var act in new Action[] {
		() => {
			var sw = System.Diagnostics.Stopwatch.StartNew();
			foreach (var item in collection)
			{
				if (item is SomeType)
				{
					current = item;
				}
			}
			sw.Stop();
			ret.Add("is Type Check", sw.Elapsed.Ticks);
		},
		() => {
			var sw = System.Diagnostics.Stopwatch.StartNew();
			foreach (var item in collection)
			{
				switch (item)
				{
					case SomeType _:
						current = item;
						break;
				}
			}
			sw.Stop();
			ret.Add("Switch", sw.Elapsed.Ticks);
		},
		() => {
			var sw = System.Diagnostics.Stopwatch.StartNew();
			foreach (var item in collection)
			{
				if (item.GetType() == typeof(SomeType))
				{
					current = item;
				}
			}
			sw.Stop();
			ret.Add("GetType() ==", sw.Elapsed.Ticks);
		},
		() => {
			var sw = System.Diagnostics.Stopwatch.StartNew();
			foreach (var item in collection)
			{
				if (item.GetType().Name == nameof(SomeType))
				{
					current = item;
				}
			}
			sw.Stop();
			ret.Add("GetType().Name ==", sw.Elapsed.Ticks);
		},
		() => {
			var sw = System.Diagnostics.Stopwatch.StartNew();
			foreach (var item in collection)
			{
				if (item.Type == nameof(SomeType))
				{
					current = item;
				}
			}
			sw.Stop();
			ret.Add("String ==", sw.Elapsed.Ticks);
		}
	}.Shuffle()) { act.RunWithNoGarbageCollection(); }

	return ret;
}

Here is the average of 30 runs, run in random orders (all times in Ticks):



Average Time (in ticks) to access N number of items

Itemsis Type CheckSwitchGetType() ==String ==GetType().Name ==
112.03412.52516.53315.759115.879
213.02512.20217.14418.95123.891
414.614.79219.57519.233131.564
815.43315.1520.11721.717133.283
1616.64217.14222.22523.792133.79
3219.61719.225.67530.937143.721
6423.93326.08331.50842.029148.576
12830.53930.84143.16765.721166.759
25645.26747.32163.763113.992202.952
51276.01782.043109.745205.667269.67
1024143.097147.991203.075389.652414.325
2048262.353283.598367.256783.25693.227
4096515.6541.212736.8671507.51261.009
81921016.4871069.2091423.2563087.9332391.017
163841975.2042122.8642929.486167.1264662.239
327683987.5554231.5045624.01712065.6279191.096
655367944.5178519.98311365.03424051.20718178.681
13107216135.07817164.27822468.90448443.85536318.929
26214432195.95734124.08345517.65198002.773192.632
52428864948.87768679.96690870.886194665.153146659.209
1048576128760.474138218.902182046.453393126.867302830.505
2097152263783.485273603.586361891.478778186.752584512.739


Let's normalize to a single approach: Type checking.



Normalized Average Time (with respect to is Type Check)

Itemsis Type CheckSwitchGetType() ==String ==GetType().Name ==
111.041.371.319.63
210.941.321.459.51
411.011.341.329.01
810.981.31.418.64
1611.031.341.438.04
3210.981.311.587.33
6411.091.321.766.21
12811.011.412.155.46
25611.051.412.524.48
51211.081.442.713.55
102411.031.422.722.9
204811.081.42.992.64
409611.051.432.922.45
819211.051.43.042.35
1638411.071.483.122.36
3276811.061.413.032.3
6553611.071.433.032.29
13107211.061.3932.25
26214411.061.413.042.27
52428811.061.432.26
104857611.071.413.052.35
209715211.041.372.952.22


Visually, this might make more sense on a log2 - log2 plot...





First off, item is SomeType is still the clear winner followed closely by switch (item){ case SomeType .... Type checking using item.GetType() == typeof(SomeType) is a little worse, and finally string comparisons are orders of magnitude slower. Once again, using compiler services is faster than string comparison. I guess reflection isn't as bad as we thought.