3 C# performance tips that surprised me
In programming, certain ideas often feel like common knowledge. They sit in the back of our minds, perhaps learned in school or simply remembered that way over time. Yet, our assumptions can sometimes mislead us. What was once true may no longer apply, or in some cases, may never have been accurate at all.
In this post, I want to share three small C# performance tips I learned this year.
When discussing performance, these aspects usually only matter if you’re working on an application where efficiency is critical. For smaller or simpler applications, it can still be nice to implement things in the best way, but it’s not necessarily required.
String concatenation: StringBuilder, ‘+’-operator or interpolation?
At school, I was always taught to use a StringBuilder
when concatenating strings, since it avoids creating new strings on every operation. The idea was that StringBuilder
is optimized under the hood to combine everything efficiently.
However, there are also other options for string concatenation in C#, like the +
operator or string interpolation. I already knew that if you’re only concatenating a few strings, these options are usually better than StringBuilder
, because creating and managing a StringBuilder
instance also costs performance and memory.
But this made me curious, how do they all really compare? 🤔 Especially the +
operator versus string interpolation.
So, like always, I made a little benchmark:
[Benchmark]
public void ConcatenationWithPlus()
{
var secondPart = "second part";
var concatenatedString = "first part " + secondPart;
}
[Benchmark]
public void ConcatenationWithInterpolation()
{
var secondPart = "second part";
var concatenatedString = $"first part {secondPart}";
}
[Benchmark]
public void ConcatenationWithStringBuilder()
{
var stringBuilder = new StringBuilder();
stringBuilder.Append("first part ");
stringBuilder.Append("second part");
var concatenatedString = stringBuilder.ToString();
}
And these were the results:
I expected StringBuilder
to be slower in this case. What surprised me was that the +
operator and string interpolation performed exactly the same.
This has everything to do with how the compiler translates these constructions. When the compiler can see what strings are being concatenated, both the +
operator and string interpolation are lowered to a call to string.Concat
. That method is already a very efficient way of combining strings, so both end up with identical performance.
So does this mean we should never use StringBuilder
again? Not quite. When the number of strings being concatenated isn’t known at compile time, for example, when you’re appending inside a loop, then the +
operator (or interpolation) can’t be optimized into a single string.Concat
. Instead, a new string is created on every iteration, which quickly adds up. In those scenarios, StringBuilder
is still the better choice.
I always thought the +
operator was ’evil’, but it turns out that in simple cases, it behaves the same as string interpolation, both are optimized, and both are perfectly fine. 👌
DateTime.Now
I never really thought much about using DateTime in C#, but once I started looking at it more closely, I noticed there’s actually a big difference in performance between using DateTime.Now
, DateTime.UtcNow
, and just caching a value once.
To test this, I made another small benchmark where I compared all three approaches. 🧪
public class DateTimePerformanceTests
{
private static readonly DateTime DateTimeNowCached = DateTime.Now;
[Benchmark]
public void DateTimeNow()
{
for (var i = 0; i < 10000; i++)
{
var x = DateTime.Now;
}
}
[Benchmark]
public void CachedDateTime()
{
for (var i = 0; i < 10000; i++)
{
var x = DateTimeNowCached;
}
}
[Benchmark]
public void DateTimeUtcNow()
{
for (var i = 0; i < 10000; i++)
{
var x = DateTime.UtcNow;
}
}
}
And here are the results:
That’s quite a difference. DateTime.Now
turned out to be by far the slowest, since it not only has to fetch the system time but also applies the local timezone conversion under the hood. DateTime.UtcNow
is noticeably faster, because it skips that conversion step. And of course, just reading a cached value is basically free compared to both.
The takeaway here is simple:
- Use
DateTime.Now
only if you really need the local time. - Prefer
DateTime.UtcNow
in performance-sensitive code, logging, or comparisons. - If you don’t need sub-millisecond precision, you can also cache the value once and reuse it.
One example where this happens often is when you’re mapping objects in a loop. I’ve seen code where each iteration assigns DateTime.UtcNow
to a property. Functionally, it works, but it means the call to UtcNow runs thousands of times unnecessarily. In cases like that, it’s much faster to get the timestamp once before the loop and reuse it.
One final note: you might want to use
DateTimeOffset.UtcNow
instead ofDateTime.UtcNow
. This makes stored values unambiguous, which is great for databases or APIs. Functionally, they represent the same instant in time, but DateTimeOffset always carries the offset with it (+00:00 for UTC). Just be aware it’s a bit slower thanDateTime.UtcNow
, since it does the same work plus wraps it in an offset. 💡
Dictionary lookups: TryGetValue vs ContainsKey + indexer
I’ve seen this pattern a lot in codebases:
if (dict.ContainsKey(key))
{
var value = dict[key];
// ...
}
At first glance this looked fine to me, but it actually performs two lookups in the dictionary: one for ContainsKey and another for the indexer. 👀
There’s a more efficient way:
if (dict.TryGetValue(key, out var value))
{
// use value
}
This does the lookup only once and tells you both whether the key exists and what the value is.
The difference is small in many cases, and to be completely honest, I like ContainsKey
more for its readability. But for performance intensive code, and with large dictionaries, it can make a difference.
However, if you don’t need the value and are only interested in knowing whether the key is in the dictionary or not, then I would still use ContainsKey
.
There you go, three things I wanted to share with you. I hope I managed to surprise you with at least one of them!