Performance crime: Concat

Quiz time

Brainstorm to begin with – which code is faster and why:

var language = new object();
var version = new object();

var a = string.Concat(itemId, language, "¤", version);
var b = string.Concat(itemId, language.ToString(), "¤", version.ToString());

Answer

Despite both lines look super familiar, a is 30% slower due to an overload with params being picked:

As you might have guessed (from previous post), params force an array allocation (which I treat as crime):

Calling ToString explicitly cuts out one third of wall clock time (1076 vs 767):

Does the code look familiar?

The sample code looks to be a suitable candidate to act as a cache key for an item (in fact, it is). Not only it produces a new string on each go, but also forces brute-force string over direct by-field comparison. We can easily fix those flaws and cook a faster version:

struct Key: IEquatable<Key>
 {
   private readonly ID _id;
   private readonly Language _language;
   private readonly Version _version;

   public Key(ID id, Language language, Version version)
    {
      _id = id;
      _language = language;
      _version = version;
     }
...
public static bool operator ==(Key a, Key b) => Equals(a._id, b._id) && Equals(a._version, b._version) && Equals(a._language, b._language);

The struct based-key uses by-field comparison and does not need allocations resulting in 4 times faster execution:

Summary

Joining strings to build a cache key might be four times as slow as doing it right with structs. You’ll tax system users with ~50ms on each page request (real-life numbers).

On the one hand, 1/20 of a second sounds very little.

On the other hand, the delay is brought by a few lines of code, whereas enterprise-level frameworks have huge code bases with non-zero chances having only one non-optimal code.

3 thoughts on “Performance crime: Concat

  1. I am curious, if there could be any way to measure it, what percentage of the performance difference is related to passing arguments by reference (object) vs. value types (string/struct/etc.).

    Like

  2. It is faster indeed but you run it inside a loop.
    Would it be worth it to refactor a web-page which has around 500-1k req/min and uses this particular function?

    What would you use instead if you have to concat 5 or more strings?

    Like

    1. Hi Emanuel, Thanks for your question. I’d like to quote Steve Souders to begin with: https://youtu.be/RwSlubTBnew?t=100

      Prior to any refactoring, I’d do a performance profiling investigation round.
      I would check if these allocates leave a footprint in overall application memory consumption patterns.
      Yes, we know this code can be x10 faster, but if that is a tiny CPU & memory consumption participant, it might make sense to focus on top culprits.

      What would I use as a key? Struct with full control on equals and hashcode method.
      Let’s take Sitecore as an example with datatabase#language#id, key logical a/b variants :
      A) Concatenated string that holds all of it;
      Every new one causes allocation. Since that is a cache key, it is likely to be stored in memory. Despite we have a few db names in total, every variation would repeat it in memory. Compare is performed per-char.

      B) Struct that holds every field separate.
      Since DB is a singleInstance, it is likely that only a few ‘web’ / ‘master’ strings reside in memory, and every cache key would re-use same object.
      Compare would be by-ref = fast
      HashCode needs a good distribution, so I would solely use ID in there.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: