Optimize All The Things

I got bored and decided to run a bunch of benchmarks.
Sep 15 2021 by James Craig

I have to say that I love listening to other devs argue about really obscure things. Most of it is bluster and can be ignored but every once in a while you hear about some optimization technique or something that is interesting. My favorites are the times where people claim that X is always faster than Y, so never do Y. The reason that I love them is because some of the claims are just bizarre. The funny bit is usually the conversation ends and that bizarre bit of advice is then past on again and again. No one ever seems to follow up on these with any tests. No one benchmarks.

One time I was told to never use foreach with arrays. It's horrible for performance was what I was told. I asked how he came by this knowledge and his response was that he read it online. I asked if they could send me the source and they threw up their hands and simply changed the subject. After they had walked away, I decided that I was actually going to test what the person said. It took all of 5 minutes to write the benchmark code and another 5 minutes to run it but after that, I discovered that they were wrong. foreach and for are about the same when dealing with arrays.

After that I wondered all of the other instances where I just took a person's word for these optimizations. How many of them were accurate? Or if they WERE accurate all those years ago, how many of them still held up? With that idea in mind, any time I was bored and had nothing to do I created a new benchmark to test some preconceived idea that I had. Eventually I started to pull some of them together and gathered them together into a repo. Now every time I hear a new speed trick or optimization technique that someone mentions, I add a test for it to see if it's remotely accurate. Much of the time, they aren't but with some I've been amazed at the speed gains that can be made. Also with each new version of .Net, I test to see what still holds true. For instance, with .Net 5 I've found the following seems to be the case:

  1. When dealing with arrays of data inside of a method, use ArrayPool to allocate.

  2. Array.Copy is generally better for smaller arrays. Larger arrays (100,000+), Buffer.MemoryCopy seems to be a better option.

  3. For arrays, just treat them as arrays when iterating over them. IEnumerable, etc. slow them down.

  4. For arrays, foreach and for have no difference at this point in time.

  5. If you can, use Array.Sort and List.Sort. They're much faster than the alternatives.

  6. The various old optimizations like bitwise math are no longer effective.

  7. Unchecked math no longer seems to be substantially faster.

  8. Structs are much faster to create than classes. Setting or getting values is about the same though.

  9. For ConcurrentDictionary, use TryGetValue over an index or key lookup.

  10. For Dictionary, the type of the key matters. Long, uint, ulong, etc. are much faster than string.

  11. Dictionary is much faster than ConcurrentDictionary. Only use ConcurrentDictionary when you need to.

  12. Dynamic vs static typing, static is about 10x faster.

  13. Byte packing is faster than using an enum but it's about 4ns vs 2ns...

  14. Fields are slightly faster than properties in certain circumstances.

  15. Dictionary is generally faster than Hashtable if using an int as the key. Hashtable seems slightly faster when dealing with string as the key.

  16. Array.Copy and List.CopyTo are much faster at copying data over than other options.

  17. Arrays are much faster than alternatives for iteration.

  18. ++x or x++ makes no difference.

  19. Use 'is' instead of any of the clever other options if you can for determining the type of an object.

  20. If you can, use List.AddRange instead of List.Add.

  21. Using a for loop with Lists is the fastest approach.

  22. If you need something that looks like a Dictionary, then just use a Dictionary...

  23. If you're going to create a list of items, use AddRange instead of feeding them into the constructor.

  24. For small items, MemoryStream is better than RecyclableMemoryStream. For larger items, RecyclableMemoryStream is much better though.

  25. In terms of what is better (best to worst): Direct Method Call > Func(=>) > Func(Method) > Cached MethodInfo > new Func(=>) > new Func(Method).

  26. For null equality, use either is or ReferenceEquals.

  27. If you need to create an object, just use new. If you can't use new, compile/cache a lambda expression.

  28. Use partitioning when doing Parallel.ForEach if you need to worry about speed but note there will be a slight memory increase.

  29. If you're reading in a whole file, create the whole file in one go. This does not apply for instances where you are streaming the file or modifying portions of it.

  30. When you can, cache type info as it's not free.

  31. When you can, use a simple Contains instead of using Regex.

  32. using pointers into an array is slightly faster than a Span.

  33. stackalloc seems to be a speed boost at larger number of items.

  34. Do not use a static constructor if you don't need to. It gets called every time you call a method on the class.

  35. Use a StringBuilder instead of string concat. Memory and speed improvements for anything 100+ concatenations.

  36. If you're formatting a string, string concat is usually faster than things like string interpolation.

  37. If you can use a StringBuilderPool. Faster and memory improvements to be had.

  38. If you are going to replace a value in a string, use string.Replace instead of StringBuilder.Replace.

  39. String.Substring works slightly better than Span.Slice.

  40. Using string.Substring works better than Trim for small strings. Larger strings, Trim works better.

  41. Vector works better than dealing with straight struct, byte array, etc. in terms of speed. Memory increases though.

Note that the above are based on tests that I will not say are perfect. Similarly these aren't like the micro benchmarks that the .Net people do to check speed improvements between versions of .Net. These are just to check differences between approaches to do the same thing. With that in mind, it would be interesting for people to add their own. So feel free to do a pull request with something that you want to test.