Yesterday, I wrote about my thoughts around optimising the comparison of a variable with multiple options and how I prefer to optimize for readability over theoretical or potential performance issues.
After some discussion on twitter about how my sample code might be optimized I thought it was worth doing some proper analysis.
The big potential optimizations are
- The removal of LINQ
- Not creating a new array for each check
- Removing the need for boxing
Using BenchmarkDotNet I was able to quickly run some comparisons to see which method would be the quickest. I tried a number of variations with the type of object and array creation.
The results.
Method | Mean | Error | StdDev |
------------------------------------- |------------:|----------:|----------:|
MultipleEquals | 0.0013 ns | 0.0041 ns | 0.0034 ns |
IsOneOfObject | 132.7228 ns | 2.2074 ns | 2.0648 ns |
IsOneOfObjectWithExistingArray | 93.5959 ns | 1.7975 ns | 1.8459 ns |
IsOneOfGeneric | 63.4804 ns | 0.8762 ns | 0.7317 ns |
IsOneOfGenericWithExistingArray | 58.5615 ns | 0.8739 ns | 0.7747 ns |
IsOneOfMyEnum | 64.2691 ns | 1.3435 ns | 1.3195 ns |
IsOneOfMyEnumWithExistingArray | 58.2238 ns | 0.9457 ns | 0.8383 ns |
IsOneOfMyEnumNoLinq | 12.1887 ns | 0.2395 ns | 0.2123 ns |
IsOneOfMyEnumNoLinqWithExistingArray | 6.2302 ns | 0.0519 ns | 0.0433 ns |
Full benchmark code is at https://gist.github.com/mrlacey/1b3eef0a9945b67883486bb9540b533d
While using multiple equality checks was by far the fastest approach, by removing LINQ, not boxing, and not creating a new array for each test I was able to achieve the best performance.
Conclusion
While using multiple equality checks is by far the fastest, I still think writing
if (MyEnum.Value1 || someVariable == MyEnum.Value2 || someVariable == MyEnum.Value3 || someVariable == MyEnum.Value6)
{
/// do something
}
is far less preferable toif (someVariable.IsOneOf[MyEnumNoLinq](targetValues))
{
/// do something
}
And the code can still be pretty darn fast.
You just might need a few overloads to get the best performance from all comparisons.
Disclaimer.
Yes, I am aware that:
- I only ran the tests with enums and you might get different results with different types.
- It might not be possible to have a fixed array to compare with each time.
- There may be further optimizations available. This is good enough for me though.
- Based on the speeds involved this does feel like a micro-optimization unless you really are making such calls many, many times in quick succession.
- I haven't looked at memory usage. If you're interested, go ahead, but standard warnings about premature micro-optimizations exist.
*** UPDATE ***
Ok, I gave in and looked at the memory profiling too:
Method | Gen 0 | Allocated |
------------------------------------- |-------:|----------:|
MultipleEquals | - | 0 B |
IsOneOfObject | 0.0558 | 88 B |
IsOneOfObjectWithExistingArray | 0.0178 | 28 B |
IsOneOfGeneric | 0.0178 | 28 B |
IsOneOfGenericWithExistingArray | - | 0 B |
IsOneOfMyEnum | 0.0178 | 28 B |
IsOneOfMyEnumWithExistingArray | - | 0 B |
IsOneOfMyEnumNoLinq | 0.0178 | 28 B |
IsOneOfMyEnumNoLinqWithExistingArray | - | 0 B |
Yeah, I think it's safe to say that it's not an issue. If you're really concerned over 28 (or even 88) bytes you're in a really constrained environment and nothing I have to say about optimization or readability will be relevant or new to you. In fact, if you're in such a constrained environment you're probably best not writing in a managed language.
Really? 11 different ways came to your mind, but not the bit-wise OR? It really amazes me what kind of articles show up on the morning brew!
ReplyDeleteIlya, I tend to use the above with string comparisons mostly so was really thinking that way. These posts came about as a result of a separate discussion focusing specifically on an instance that used an enum so that's where the above examples originated. Yes, with specific types there will inevitably be specific optimizations. I was more interested in the general approach and am sorry if you were misled.
Delete