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 astring
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
Items | is Type Check | Switch | GetType() == | String == | GetType().Name == |
---|---|---|---|---|---|
1 | 12.034 | 12.525 | 16.533 | 15.759 | 115.879 |
2 | 13.025 | 12.202 | 17.144 | 18.95 | 123.891 |
4 | 14.6 | 14.792 | 19.575 | 19.233 | 131.564 |
8 | 15.433 | 15.15 | 20.117 | 21.717 | 133.283 |
16 | 16.642 | 17.142 | 22.225 | 23.792 | 133.79 |
32 | 19.617 | 19.2 | 25.675 | 30.937 | 143.721 |
64 | 23.933 | 26.083 | 31.508 | 42.029 | 148.576 |
128 | 30.539 | 30.841 | 43.167 | 65.721 | 166.759 |
256 | 45.267 | 47.321 | 63.763 | 113.992 | 202.952 |
512 | 76.017 | 82.043 | 109.745 | 205.667 | 269.67 |
1024 | 143.097 | 147.991 | 203.075 | 389.652 | 414.325 |
2048 | 262.353 | 283.598 | 367.256 | 783.25 | 693.227 |
4096 | 515.6 | 541.212 | 736.867 | 1507.5 | 1261.009 |
8192 | 1016.487 | 1069.209 | 1423.256 | 3087.933 | 2391.017 |
16384 | 1975.204 | 2122.864 | 2929.48 | 6167.126 | 4662.239 |
32768 | 3987.555 | 4231.504 | 5624.017 | 12065.627 | 9191.096 |
65536 | 7944.517 | 8519.983 | 11365.034 | 24051.207 | 18178.681 |
131072 | 16135.078 | 17164.278 | 22468.904 | 48443.855 | 36318.929 |
262144 | 32195.957 | 34124.083 | 45517.651 | 98002.7 | 73192.632 |
524288 | 64948.877 | 68679.966 | 90870.886 | 194665.153 | 146659.209 |
1048576 | 128760.474 | 138218.902 | 182046.453 | 393126.867 | 302830.505 |
2097152 | 263783.485 | 273603.586 | 361891.478 | 778186.752 | 584512.739 |
Let's normalize to a single approach: Type checking.
Normalized Average Time (with respect to is Type Check)
Items | is Type Check | Switch | GetType() == | String == | GetType().Name == |
---|---|---|---|---|---|
1 | 1 | 1.04 | 1.37 | 1.31 | 9.63 |
2 | 1 | 0.94 | 1.32 | 1.45 | 9.51 |
4 | 1 | 1.01 | 1.34 | 1.32 | 9.01 |
8 | 1 | 0.98 | 1.3 | 1.41 | 8.64 |
16 | 1 | 1.03 | 1.34 | 1.43 | 8.04 |
32 | 1 | 0.98 | 1.31 | 1.58 | 7.33 |
64 | 1 | 1.09 | 1.32 | 1.76 | 6.21 |
128 | 1 | 1.01 | 1.41 | 2.15 | 5.46 |
256 | 1 | 1.05 | 1.41 | 2.52 | 4.48 |
512 | 1 | 1.08 | 1.44 | 2.71 | 3.55 |
1024 | 1 | 1.03 | 1.42 | 2.72 | 2.9 |
2048 | 1 | 1.08 | 1.4 | 2.99 | 2.64 |
4096 | 1 | 1.05 | 1.43 | 2.92 | 2.45 |
8192 | 1 | 1.05 | 1.4 | 3.04 | 2.35 |
16384 | 1 | 1.07 | 1.48 | 3.12 | 2.36 |
32768 | 1 | 1.06 | 1.41 | 3.03 | 2.3 |
65536 | 1 | 1.07 | 1.43 | 3.03 | 2.29 |
131072 | 1 | 1.06 | 1.39 | 3 | 2.25 |
262144 | 1 | 1.06 | 1.41 | 3.04 | 2.27 |
524288 | 1 | 1.06 | 1.4 | 3 | 2.26 |
1048576 | 1 | 1.07 | 1.41 | 3.05 | 2.35 |
2097152 | 1 | 1.04 | 1.37 | 2.95 | 2.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.