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.
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.).
LikeLike
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?
LikeLike
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.
LikeLike