I wanted to add to the above answers, that there are some scenarios (depends on how the data is distributed) in which a nested dictionary is much better than a composite key dictionary in terms of memory footprint (which in turn might lead to better performance overall).
The reason for that is that the nesting can save you the need of saving duplicated values for the keys, which in large dictionaries will make the footprint of the extra dictionaries negligible.
For example, say I need a dictionary with a composite key of (male/female),(baby/young/old),(age).
Let's save some values with a dictionary of composite keys:
(male, baby, 1)
(male, baby, 2)
(male, baby, 3)
(male, young, 21)
(male, young, 22)
(male, young, 23)
(male, old, 91)
(male, old, 92)
(male, old, 93)
(female, baby, 1)
(female, baby, 2)
(female, baby, 3)
(female, young, 21)
(female, young, 22)
(female, young, 23)
(female, old, 91)
(female, old, 92)
(female, old, 93)
Now let's save the same values in a dictionary of dictionaries:
male -> baby -> 1
2
3
young -> 21
22
23
old -> 91
92
93
female -> baby ->1
2
3
young -> 21
22
23
old -> 91
92
93
In the composite key approach I save a copy of "male" and "female" 9 times, as opposed to a single copy in the dictionary of dictionaries.
In fact, I saved 54 items vs 26 items, getting twice the memory footprint. The example also helps to visualize the difference, see how much "empty" space there is in the second sample compared to the first, those are all values that we didn't need to save.
And for those which are still not convinced, here's a sample test:
Dictionary<Tuple<int, int, int>, int> map1 = new Dictionary<Tuple<int, int, int>, int>();
Dictionary<int, Dictionary<int, Dictionary<int, int>>> map2 = new Dictionary<int, Dictionary<int, Dictionary<int, int>>>();
public void SizeTest()
{
for (int x = 0; x < 30; x++)
{
for (int y = 0; y < 100; y++)
{
for (int z = 0; z < 600; z++)
{
addToMap1(x, y, z, 0);
addToMap2(x, y, z, 0);
}
}
}
int size1 = GetObjectSize(map1);
int size2 = GetObjectSize(map2);
Console.WriteLine(size1);
Console.WriteLine(size2);
}
private void addToMap1(int x, int y, int z, int value)
{
map1.Add(new Tuple<int, int, int>(x, y, z), value);
}
private void addToMap2(int x, int y, int z, int value)
{
map2.GetOrAdd(x, _ => new Dictionary<int, Dictionary<int, int>>())
.GetOrAdd(y, _ => new Dictionary<int, int>())
.GetOrAdd(z, _ => value);
}
private int GetObjectSize(object TestObject)
{
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
byte[] Array;
bf.Serialize(ms, TestObject);
Array = ms.ToArray();
return Array.Length;
}
public static TResult GetOrAdd<TKey, TResult>(this Dictionary<TKey, TResult> map, TKey key, Func<TKey, TResult> addIfMissing)
{
TResult result;
if (!map.TryGetValue(key, out result))
{
result = addIfMissing(key);
map[key] = result;
}
return result;
}
This test returns ~30MB vs ~70MB in favor of the dictionary of dictionaries.