Type checking with strings in C#

Posted in software by Christopher R. Wirz on Thu Feb 02 2017

A developer made a plugin architecture in which some plugins were built in the same library as the microkernel / core in order to provide a default implementation. Cool! The weird thing is that type checking was still being done using a public member called "Type". Here is an example of the implementation:


class BaseType
{
	public virtual string Type { get => "BaseType"; }
}
class SomeType : BaseType
{
	public override string Type => "SomeType";
}
class SomeOtherType : BaseType
{
	public override string Type => "SomeOtherType";
}

Yes, that works, but isn't it slower than using the object oriented features of the language and the optimization of the compiler? I think so, but let's see. To do this, we'll have to test string comparisons against type checking.

For type checking, we'll use the is operator.
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.Type == "SomeType")
				{
					current = item;
				}
			}
			sw.Stop();
			ret.Add("String ==", sw.Elapsed.Ticks);
		},
		() => {
			var sw = System.Diagnostics.Stopwatch.StartNew();
			foreach (var item in collection)
			{
				if (item.Type.Equals("SomeType"))
				{
					current = item;
				}
			}
			sw.Stop();
			ret.Add("String Equals", sw.Elapsed.Ticks);
		},
		() => {
			var sw = System.Diagnostics.Stopwatch.StartNew();
			foreach (var item in collection)
			{
				if (item.Type.Equals("SomeType", StringComparison.InvariantCultureIgnoreCase))
				{
					current = item;
				}
			}
			sw.Stop();
			ret.Add("String Equals Invariant", sw.Elapsed.Ticks);
		},
		() => {
			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);
		}
	}.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

String ==String EqualsString Equals Invariantis Type Check
111.92513.48745.04710.392
212.86415.16748.4411.325
413.69616.1550.6512.475
816.08317.55851.63713.183
1618.06720.40863.5616.217
3223.724.53580.20216.425
6432.84635.577112.3919.675
12853.36158.681174.9627.106
25692.01999.518301.6940.096
512177.897180.638553.35168.974
1024338.904356.2871066.198119.991
2048682.336707.8192114.759232.5
40961262.0651407.7954029.673455.214
81922497.2232624.0117939.168899.913
163845013.0535628.33615733.7581752.364
3276810004.3410333.58130981.6493359.26
6553620146.15820703.96864612.336623.143
1310724006841892.642124974.11613459.447
2621448061886399.627253479.96827839.99
524288161915.789168553.032513144.44758021.25
1048576325638.787338887.9581021087.558108224.547
2097152649893.726676123.9162042077.179216568.176


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



Normalized Average Time (with respect to is Type Check)

itemsString ==String EqualsString Equals Invariantis Type Check
41.147517321016171.297825250192464.334776751347191
81.13589403973511.339249448123624.277262693156731
161.097875751503011.294589178356714.060120240480961
321.219980277630281.331866798149133.916938481377531
641.114077819572051.258432509095393.919343898378251
1281.442922374429221.49375951293764.882922374429221
2561.669428208386281.808233799237615.712325285895811
5121.968604736958612.164871246218556.454659484984871
10242.294967079010382.481993216280937.524191939345571
20482.579189259721052.618928871748778.022602719865461
40962.824411830887322.969281029410548.885649757065111
81922.934778494623663.044382795698929.09573763440861
163842.772465258098393.092600403326798.852260694969841
327682.774960468400832.915849643243298.822150585667721
655362.860737266914863.211853245102048.978590064621281
1310722.978138042306943.076148020695039.222760072158751
2621443.041782126703293.126003469953779.755539024297081
5242882.97694251480023.112508411378279.285234081311071
10485762.895762534397463.10343599261359.104887178479591
20971522.790629105715582.905022418510468.84407776461211
41943043.008917995286233.13134097017759.434897962659061
83886083.000873618661313.121991090694699.429257874896631


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





These results are really interesting. First we see that type checking is the fastest (yay, I was right!). There has to be some compiler optimizer helping with that. Next we see that == is the fastest comparison. While Equals is a little slower, Equals Invariant is more than 2x slower than that.

Why are some comparisons faster than others? Let's look at the source code...

First we look at the equals operator (==).


public static bool operator ==(string? a, string? b)
{
	if ((object)a == b)
	{
		return true;
	}
	if ((object)a == null || (object)b == null || a!.Length != b!.Length)
	{
		return false;
	}
	return EqualsHelper(a, b);
}

Not too bad. But it's good to know that EqualsHelper goes through every character and compares its numeric value.

Now check the Equals method by itself. That's fairly straight-forward.


public bool Equals(string? value)
{
	if ((object)this == value)
	{
		return true;
	}
	if ((object)value == null)
	{
		return false;
	}
	if (Length != value!.Length)
	{
		return false;
	}
	return EqualsHelper(this, value);
}

Now check the Equals operator with Comparison type (for Equals Invariant).


public bool Equals(string? value, StringComparison comparisonType)
{
	if ((object)this == value)
	{
		CheckStringComparison(comparisonType);
		return true;
	}
	if ((object)value == null)
	{
		CheckStringComparison(comparisonType);
		return false;
	}
	switch (comparisonType)
	{
	case StringComparison.CurrentCulture:
	case StringComparison.CurrentCultureIgnoreCase:
		return CultureInfo.CurrentCulture.CompareInfo.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType)) == 0;
	case StringComparison.InvariantCulture:
	case StringComparison.InvariantCultureIgnoreCase:
		return CompareInfo.Invariant.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType)) == 0;
	case StringComparison.Ordinal:
		if (Length != value!.Length)
		{
			return false;
		}
		return EqualsHelper(this, value);
	case StringComparison.OrdinalIgnoreCase:
		if (Length != value!.Length)
		{
			return false;
		}
		return EqualsOrdinalIgnoreCase(this, value);
	default:
		throw new ArgumentException(SR.NotSupported_StringComparison, "comparisonType");
	}
}

Does this difference explain the 16x difference in speed? Somewhat. == and Equals are nearly the same but == is faster. I guess this is one of those many cases where static methods are faster than instance methods.