Levi’s Men’s Trunk TRUNK (Pack of 3)
Reduced to kr130.50
Best deals on Amazon.se week 16/2025
History of the World Map by Map Hardcover
Reduced to kr180.02. Great book about history
Anker 335 67W Charger, High Performance Power Supply with PIQ 3.0, 3-Port Fast Charger, PD Charger for iPhone 14, MacBook Pro, iPad, Galaxy, Pixel and More (USB-C to Cable
Reduced to kr299
TERRAOLIVE – extra virgin olive oil, olive variety, mild body, from Spain, Montes de Toledo, recycled PET container – 5L
Reduced to 350kr
Shorten your cache keys, please
In a recent customer engagement, I have looked into a customer where their memory usage is abnormally high. Among other findings, I think one was not very well known. But as they say, small details can make a big difference – and that small detail in today’s post is the cache key.
Let’s put it to test. This is what I asked Copilot to scaffold it, and then just some small adjustments. The test is to add 10.000 items to cache, and then read each entry 10 times. One test with a very short prefix, and one with a long (but not very long) one.
using System;
using System.Runtime.Caching;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
namespace MemoryCacheBenchmarkDemo
{
// Use MemoryDiagnoser to capture memory allocation metrics during benchmarking.
[MemoryDiagnoser]
public class MemoryCacheBenchmark
{
private const int Iterations = 10_000;
private const int ReadIterations = 10;
[Benchmark(Description = "MemoryCache with Short Keys")]
public void ShortKeysBenchmark()
{
using (var cache = new MemoryCache("ShortKeysCache"))
{
const string Prefix = "K";
// Insertion phase using short keys (e.g., "K0", "K1", ...)
for (int i = 0; i < Iterations; i++)
{
string key = Prefix + i;
cache.Add(key, i, DateTimeOffset.UtcNow.AddMinutes(5));
}
// Retrieval phase for short keys.
for (int j = 0; j < Iterations; j++)
{
int sum = 0;
for (int i = 0; i < ReadIterations; i++)
{
string key = Prefix + i;
if (cache.Get(key) is int value)
{
sum += value;
}
}
// Use the result to prevent dead code elimination.
if (sum == 0)
{
throw new Exception("Unexpected sum for short keys.");
}
}
}
}
[Benchmark(Description = "MemoryCache with Long Keys")]
public void LongKeysBenchmark()
{
using (var cache = new MemoryCache("LongKeysCache"))
{
const string Prefix = "ThisIsAVeryLongCacheKeyPrefix_WhichAddsExtraCharacters_IsThisLongEnoughIAmNotSure";
// Insertion phase using long keys.
// Example: "ThisIsAVeryLongCacheKeyPrefix_WhichAddsExtraCharacters_0", etc.
for (int i = 0; i < Iterations; i++)
{
string key = Prefix + i;
cache.Add(key, i, DateTimeOffset.UtcNow.AddMinutes(5));
}
// Retrieval phase for long keys.
for (int j = 0; j < Iterations; j++)
{
int sum = 0;
for (int i = 0; i < ReadIterations; i++)
{
string key = Prefix + i;
if (cache.Get(key) is int value)
{
sum += value;
}
}
// Use the result to prevent dead code elimination.
if (sum == 0)
{
throw new Exception("Unexpected sum for short keys.");
}
}
}
}
}
public class Program
{
public static void Main(string[] args)
{
// Executes all benchmarks in MemoryCacheBenchmark.
BenchmarkRunner.Run<MemoryCacheBenchmark>();
}
}
}

And the difference. Not only the short key is faster, you end up with considerably less allocations (and therefore, Garbage collection)
The only requirement for cache key is that it’s unique. It might make sense to code it in a way that if you have to ever look into memory dumps (I wish you never have to, but it’s painful yet fun experience) – you know what cache entry is this. For example in Commerce we have this prefix for order cache key:
EP:EC:OS:
And that’s it. EP is short hand for EPiServer (so we know it’s us), EC is for eCommerce, and OS is for Order System. (I know, it’s been that way for a very long time for historical reasons, and nobody bothers to change it)
So next time you adding some cache to your class, make sure to use the shortest cache key as possible. It’s not micro optimization. If you know it’s better, why not?
A curious case of free objects
It’s been a long time since I posted some memory dump investigation. Not that I haven’t done that, but it’s, most of the times, hard to tell a good story. In many cases we have a gotcha moment and in the context of the work we were doing, very exciting, but to tell a story to you who don’t want to be bothered with the underlying details (and we usually can’t share a lot of them), the story becomes much less exciting.
In many cases memory dump is an aid of the investigation, but not the only tool. It is easy to see what the app was doing at the exact moment the memory dump was taken, but when it’s an ongoing process, harder to pinpoint the cause. Nevertheless, it’s a powerful tool to at least, limit the scope of what you should look into.
A colleague reached out to me the other day because a customer has raising memory over time.

Normally you would want the memory to be relatively stable over time. An increasing memory over time should raise an eyebrow, especially if it’s not eventually reaching a plateau. As this is a customer that we just engaged with some other memory allocations, we wanted to make sure that everything was as it should. Another colleague helped to collect some memory dump, so game is on.
What immediately puzzles me was that there was a lot of free objects in the memory dump. That was indication of a GC happened. Not much left for us to track the source of memory allocations. Fine, let’s take another memory dump 30 minutes later. And it’s … the same.
7c343a686798 201,951 24,234,120 EPiServer.Core.Html.StringParsing.ContentFragment
7c34431d93a8 59,155 36,690,528 System.Collections.Generic.Dictionary<System.String, EPiServer.Core.PropertyData>+Entry[]
7c3438e63230 534,370 42,749,600 EPiServer.Core.PropertyLongString
7c3436362a28 202,652 107,559,201 System.Byte[]
7c34356d7c18 158,223 1,082,118,840 System.String[]
7c34355cd7c8 10,944,050 1,235,497,138 System.String
5b4faa00d1b0 8,039,645 11,560,389,656 Free
Total 28,779,604 objects, 14,685,150,235 bytes
What could potentially create that many free objects and leave them floating around. And memory keeps raising? The free objects indicating that there have been some heavy Garbage collection. But we can’t track where those come from because they were “freed”, rendering !gcroot useless.
I was pulling my hairs for a while (not that I have that many hairs left to pull). But then I realize, could these be big objects allocated in LOH (Larger objects heap). If they are big enough (more than 85.000 bytes in size), they will not be be freed when no longer referenced, but they are not “moved” (e.g. compacted).
0:000> !dumpheap -stat -mt 5b4faa00d1b0 -min 85000
Statistics:
MT Count TotalSize Class Name
5b4faa00d1b0 53 237,914,208 Free
Total 53 objects, 237,914,208 bytes
Hmm, we have 53 big object with more than 200MB in size, so averaging around 4MB per object. Let’s see the biggest objects by removing the -stat
option
0:000> !dumpheap -mt 5b4faa00d1b0 -min 85000
Address MT Size
7bf42b10e140 5b4faa00d1b0 97,264 Free
7bf42bd1bb50 5b4faa00d1b0 2,319,816 Free
7bf42c8521a0 5b4faa00d1b0 586,096 Free
7bf42ca8d448 5b4faa00d1b0 91,064 Free
7bf42d71bdd8 5b4faa00d1b0 4,396,576 Free
7bf42dc0a5a8 5b4faa00d1b0 2,428,160 Free
7bf42e041680 5b4faa00d1b0 4,639,520 Free
7bf42faad6e8 5b4faa00d1b0 2,527,600 Free
7bf43057f0c8 5b4faa00d1b0 2,857,968 Free
7bf431c98d60 5b4faa00d1b0 2,609,160 Free
7bf4321c2a08 5b4faa00d1b0 4,194,392 Free
7bf4325e2a78 5b4faa00d1b0 3,153,664 Free
7bf43a747550 5b4faa00d1b0 662,736 Free
7bf44c90f528 5b4faa00d1b0 13,684,008 Free
7bf44da5b780 5b4faa00d1b0 3,578,672 Free
7bf484525ad8 5b4faa00d1b0 386,352 Free
7bf4845c8710 5b4faa00d1b0 15,650,872 Free
7bf485522248 5b4faa00d1b0 2,131,104 Free
7bf489000028 5b4faa00d1b0 5,767,368 Free
7bf489597aa8 5b4faa00d1b0 2,568,696 Free
7bf4b9c00028 5b4faa00d1b0 6,029,488 Free
7bf4bc000060 5b4faa00d1b0 12,785,496 Free
7bf4bcc517d0 5b4faa00d1b0 117,106,864 Free
7bf4c7400028 5b4faa00d1b0 2,854,216 Free
7bf4c7ae2930 5b4faa00d1b0 2,180,536 Free
7bf4c7d0d260 5b4faa00d1b0 3,539,256 Free
7bf4d7000028 5b4faa00d1b0 14,066,712 Free
7bf4d7e0b350 5b4faa00d1b0 871,840 Free
The biggest object is more than 100MB. Could there be any big objects that were alive of that size? Let’s go with !dumpheap -min 100000000
to see if we find any
!dumpheap -min 100000000
Address MT Size
7bf4bcc517d0 5b4faa00d1b0 117,106,864 Free
7bf724c00048 7c34356d7c18 1,073,741,848
Statistics:
MT Count TotalSize Class Name
5b4faa00d1b0 1 117,106,864 Free
7c34356d7c18 1 1,073,741,848 System.String[]
Total 2 objects, 1,190,848,712 bytes
The result turned out to be better than expected. We found two object – one is the biggest free object, and the one one is an array of string with size of almost 1GB. It has 134217728 elements, which is … a lot. (But also a lot of empty elements, more below)
0:000> !dumpobj /d 7bf724c00048 Name: System.String[] MethodTable: 00007c34356d7c18 EEClass: 00007c343551bee0 Tracked Type: false Size: 1073741848(0x40000018) bytes Array: Rank 1, Number of elements 134217728, Type CLASS (Print Array) Fields: None
Let’s hope this is not a free object waiting to be collected. !gcroot 7bf724c00048
might give us some clue
-> 7bf42810a4a8 Microsoft.Extensions.Http.DefaultHttpClientFactory
-> 7bf429868210 Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope
-> 7bf429882b78 System.Collections.Generic.List<System.Object>
-> 7bf429cf3280 System.Object[]
-> 7bf4299a5550 SomeNamespace.Services.CustomMemoryCache
-> 7bf4299a55a8 System.Collections.Generic.List<System.String>
-> 7bf724c00048 System.String[]
A bit of dotPeek reveals the code
public class CustomMemoryCache : MemoryCache, IMemoryCache, IDisposable
{
private List<string> _cacheNames = new List<string>();
public List<string> GetKeys() => this._cacheNames;
public new ICacheEntry CreateEntry(object key)
{
this._cacheNames.Add(key.ToString());
return base.CreateEntry(key);
}
As you can see the _cacheNames
will add the cache key to a list. Because List<T>
is not unique, whenever a new cache item is added, the key will be added to the list (even if it was a duplicate), so it becomes ever growing. In the previous memory dump (that was taken 30 minutes earlier), there were 81928424 entries in the list. As this is strongly referenced, the list will remain forever and only increases over time.
Now the funny part – because the List<T>
has a limit of 2GB on the array, but because string is a reference types, each element in the array is simply a reference of it – so 8 bytes per element. You can see that is a lot. Not only that, there are two things to know about List<T>
List<T>
will create a new array, double in size copy the items over once it’s filed the internal array with items. The old array will become unreachable and ready to be GC’ed the next time. Which is also why we might have a lot of empty items in the list.- Once the array grows large enough, they will be created in the Large Object Heap (LOH). The threshold is 85.000 bytes, so around 10k items in the list (because each element is 8 bytes, they do not hold the actual string object, but a reference to it). After that, each time the list is doubled, a new object will be created, and the old list will remain on LOH as a free memory object. They will (almost) never be compacted, unless there was a need to create a big object in which they will be overwritten.
The fix in this case is to simply remove _cacheNames
, as it does not serve a purpose. It took a while for the customer to deploy the fix, but when they do, the result was … impressive

Before the fix, the memory was ever increasing until the app needs to be restarted. After that, it remain below 3GB.
Moral of the story
- Keep looking.
- If there are a lot of big, free objects, most likely, there are live objects of equal size that can give you some ideas.
Best deals on Amazon.se week 13/2025
Philips OneBlade – modell QP2824/30
Reduced to kr419.99
Panasonic SD-YR2550 Bread Maker
Reduced to kr2,061.34
Gardena solcellsdriven bevattning AquaBloom sats: Solcellsdrivet bevattningssystem för balkong- och blomlådor, upp till 4 meters höjd, oberoende av årstiden (13300-20)
Reduced to kr697.38
Best deals on Amazon.se week 12/2025
Philips Sonicare Original G3 Premium Gum Care Soniska tandborsthuvuden i standardutförande – 8 förpackningar i vitt
Reduced to kr497.99
Loading sos.dll to Windbg
Windbg is still the golden standard to troubleshoot memory dumps. and sos extension is still the essential tool to help with managed memory dump investigation. With .NET Framework (4.8 or earlier), it could be as easy as .loadby sos clr. But things got a little trickier with .NET Core, as you need to install dotnet-sos for the extension, and that could cause some confusions to get it work.
So, the thing is that you can’t (or rather, can’t stop at) install dotnet-sos like this
dotnet tool install -g dotnet-sos
Which will install it in a weird path like this C:\Users\quma.dotnet\tools.store\dotnet-sos\9.0.607501\dotnet-sos\9.0.607501\tools\net6.0\win-x64
And if you run the command to load it like this
.load C:\Users\vimvq\.dotnet\tools\.store\dotnet-sos\8.0.510501\dotnet-sos\8.0.510501\tools\net6.0\any\win-x64\sos.dll
You can’t run any command that you’re used to, like !dumpheap
0:000> !dumpheap
Error: Fail to create host ldelegate 80070002
Error: ICLRRuntimeHost::ExecuteInDefaultAppDomain failed 80070002
Unrecognized command 'dumpheap'
Why? Because now sos.dll is simply a loader, the commands like !dumpheap
is implemented in a different dll that is not in the same folder with sos.dll. If you open the folder, it looks like this

So you don’t have the assemblies that contain the commands you need.
What you want is to install it with
dotnet-sos install
so it will be installed in a path like C:\Users\quma.dotnet\sos
, and the folder should look like this

And then you can run .load C:\Users\quma\.dotnet\sos\sos.dll
in WinDBG and everything should run as expected. Note that if you run .load
command with wrong path before, you need to restart the debugging process. (Press Stop Debugging and Ctrl+D to reload the memory dump.
Best deal on Amazon.se week 10/2025
Assassins Creed Mirage PS5
Reduced to kr199.00
Hultafors 827023 Crowbar, Red
Reduced to kr67.00
Belkin BoostCharge Wireless Magnetic Car Phone Holder Compatible with iPhone 15 14 13 12 Plus Pro Pro Max Mini and More with MagSafe (Cable and Power Supply Adapter Included)
Reduced to kr96.00
Belkin charger for Apple Watch, MFi certified wireless fast charging travel pad with bedside mode
Reduced to kr126.00
Best deals on Amazon.se week 9/2025
Samsonite Men’s Mysight – Laptop Rucksack Laptop Backpacks
Reduced to 632kr
Tapo D210, Wireless Video Clock, 3MP Battery Operated Ring Bell, 160 Degree Ultra-Wide View, Color Night Vision, AI Detection, Weatherproof IP65
Reduced to 643.70kr
Best deals on Amazon.se week 7/2025
Knipex Assembly Pack (SB Board/Blister) 00 20 11
Reduced to kr591.20
Kistenberg Battery Box KBB03-115 Spaces For Batteries
Reduced to kr91.17