I have two dictionaries with different value types: Dictionary<int, string[]> and Dictionary<int, int[]>. Suppose we generate random arrays in a loop and insert them to the dictionaries (in C#).
var d1 = new Dictionary<int, string[]>();
var d2 = new Dictionary<int, int[]>();
var sw = Stopwatch.StartNew();
for (int i = 0; i < 40000000; i++)
{
string[] sarr = new string[10];
for (int j = 0; j < 10; j++)
{
sarr[j] = j.ToString();
}
int[] iarr = new int[10];
for (int j = 0; j < 10; j++)
{
iarr[j] = j;
}
d1[i] = sarr; // (1)
d2[i] = iarr; // (2)
}
sw.Stop();
Note the last two lines of the for loop. When I run the above code, it takes about 13.9 sec on my machine. Now, when I comment out only (1), then it takes about 13.7 sec. And if I comment out only (2), then it takes about 20 sec. In other words, by removing (2) it becomes much slower! I repeated this multiple times, and I can confirm that the behavior is consistent.
Can anyone please explain how this is even possible?
I did this experiment because I noticed that inserting string[] is slower than inserting int[] even though I am using the same key in the two dictionaries. I would like to know why inserting string [] can be slower than inserting int[], too.
So my quetion is twofold: (1) how come removing a line from the above code can make things slower, (2) why inserting string[] is slower than inserting int[]?
FYI, I am using the latest .NET 5 (5.0.103). I tried the code on both Windows and Linux, and the behavior was the same. I consistently see the same problem using either debug or release mode.
When I diff ILs of commented out version vs. original, the commented out version has no call to set_Item function of the dictionary as expected. Other things are more or less the same.
IL_0079: ldloc.1 // dictionary2
IL_007a: ldloc.3 // key
IL_007b: ldloc.s numArray
IL_007d: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32[]>::set_Item(!0/*int32*/, !1/*int32[]*/)
IL_0082: nop
The above part was removed when I commented out (2) for example.
To help repro this problem, I created a simple repo with Benchmark.NET: https://github.com/sangkilc/TestDictionary. In this repo, I reduced the number of iterations (from 40M to 4M) because it takes too long.
On my machine the results are:
.NET Core SDK=5.0.103
[Host] : .NET Core 5.0.3 (CoreCLR 5.0.321.7212, CoreFX 5.0.321.7212), X64 RyuJIT
DefaultJob : .NET Core 5.0.3 (CoreCLR 5.0.321.7212, CoreFX 5.0.321.7212), X64 RyuJIT
| Method | Mean | Error | StdDev |
|--------- |--------:|---------:|---------:|
| TestBoth | 1.269 s | 0.0222 s | 0.0208 s |
| TestOne | 1.381 s | 0.0257 s | 0.0241 s |
As per @TheodorZoulias's observation, if I modify d2 to a 2D array, then the difference becomes more significant:
| Method | Mean | Error | StdDev |
|--------- |--------:|---------:|---------:|
| TestBoth | 1.137 s | 0.0195 s | 0.0163 s |
| TestOne | 1.373 s | 0.0246 s | 0.0345 s |
This is not an answer, just wanted to show off some pictures
I have the code compiled in Release for two of your scenarios:
Note that I removed Stopwatch related code, because we're only interested in Dictionary.
I have dotTrace on my machine, so I got some profiling results (line-by-line).
For the both scenario:
For the (1) only scenario:
Since you mentioned that the pattern is consistent, I ran the profiling only once.
From the results, we could tell functions related to Dictionary are not the main contributors to the so called "weird performance behavior", GC probably is.