【问题标题】:ConcurrentDictionary: Proper small initial capacityConcurrentDictionary:适当的小初始容量
【发布时间】:2020-05-17 05:08:11
【问题描述】:

我一直缺乏为ConcurrentDictionary<TKey, TValue> 选择适当初始容量的指导。

我的一般用例是您真正想要执行以下操作但不能执行的情况:

public static class StaticCache<T>
{
    public static readonly Action CompiledExpression = ...;
}

这种基于泛型的方法避免了字典查找,但只有当我们在编译时总是知道所需的类型时才能使用。如果我们在运行时只知道Type,我们就不能再使用这种方法了。下一个竞争者是ConcurrentDictionary&lt;TKey, TValue&gt;

documentation 声明:

默认容量(DEFAULT_CAPACITY),代表初始桶数,是在构建大字典时,在非常小的字典大小和调整大小的数量之间进行权衡。此外,容量不应被小素数整除。默认容量为 31。

我的预期元素数量往往相对较少。有时小到 3 或 5 个,有时可能只有 15 个。因此:

  • 应用程序生命周期内的插入次数非常最少,保证 [写入] 并发级别为 1,从而优化紧凑性和读取操作。
  • 最好使用尽可能小的内存占用,以优化缓存行为。

由于默认初始容量为 31,我们可以通过使用较小的初始容量来潜在地减少对缓存的影响(以及增加字典保留在缓存中的可能性)。

这引发了以下问题:

  1. 容量实际上是什么意思?

    • (A) 字典不需要增长来容纳这么多元素
    • (B) A 的固定百分比,取决于字典的最大“丰满度”,例如75%?
    • (C) A 或 B 的近似值,取决于实际内容的哈希码如何分布它们?
  2. 什么构成和不构成“小素数”?显然,31 没有。 11吗? 17吗? 23 吗?

  3. 如果我们碰巧想要一个接近小素数的容量,我们可以选择什么容量呢?我们是简单地选择最接近的非素数,还是素数更适合容量,我们真的应该选择更大的素数吗?

【问题讨论】:

  • (2) 当它说the capacity should not be divisible by a small prime number. 时,它暗示other than itself,所以31不能被一个小的素数整除,因为31 本身就是一个素数。 (3) 见我对 (2) 的评论!
  • 这些都是很好的问题,我不禁想到,如果有人投资于整个事情实际上代表共同利益来处理基准,那就更好了。提示提示。 :-P
  • 如果插入次数很少,而性能确实是问题所在,我建议使用 ConcurrentDictionary&lt;&gt; 甚至 Dictionary&lt;&gt; 以外的其他内容。它们并不完全是缓存友好的。在不可变排序数组行中有一些东西,即完全(原子地)复制+插入替换是更好的选择。
  • 我只是想基本上说一下@nothrow 刚才所说的内容,并补充说ImmutableDictionary 是另一种选择(如果您想按类型索引,比自己维护数组稍微简单一些)。即使如此,通过一个小数组进行线性搜索也可以比字典查找更快甚至更快,具体取决于具体情况(但无论如何使用Dictionary-like 类型来涵盖您 do的情况> 如果不能保证,拥有超过 X 个元素比依赖过早优化要好。)
  • 您是否进行了任何内存分析以查看设置容量 (1-15) 的实际效果?

标签: c# primes concurrentdictionary capacity


【解决方案1】:

reference source 中为ConcurrentDictionary&lt;TKey, TValue&gt; 你可以看到:

Node[] buckets = new Node[capacity];

所以,容量就是哈希表的有效大小。不考虑“丰满度”。这个数字的唯一预处理是:

if (capacity < concurrencyLevel)
{
    capacity = concurrencyLevel;
}

其中concurrencyLevel 由您通过构造函数参数定义,或者是定义为PlatformHelper.ProcessorCount 的默认并发级别。

Dictionary&lt;TKey,TValue&gt; 对容量的处理方式不同。这里初始化为

private void Initialize(int capacity) {
    int size = HashHelpers.GetPrime(capacity);
    buckets = new int[size];
    ...
}

HashHelpers.GetPrime 获得大于或等于指定容量的最小素数。高达7199369 的素数取自预先计算的数组。较大的计算是“艰难的方式”。有趣的是,考虑的最小素数是3

很遗憾,HashHelpers 是一个内部类。

如果我理解正确的话,两种实现都会根据冲突的数量而不是基于特定的填充因子(“填充度”)来调整哈希表的大小。

如果你愿意

  • 优化速度:取一个初始容量,它比预期的最大字典大小大 30% 左右。这样可以避免调整大小。
  • 优化内存占用:取一个比最小预期大小大约 30% 的素数。
  • 速度和内存占用之间的平衡:从上面取一个介于两者之间的数字。但无论如何,取一个素数。

【讨论】:

  • 这 30% 来自哪里,如果预期的字典大小非常小,这有多大的相关性?
  • 当达到高度填充时,碰撞次数会急剧增加。这不是一个确切的数字,但它是一个在实践中证明了自己的数字。这是一个大概的数字。根据Hash table (Wikipedia),Java 10 中HashMap 的默认负载因子为 0.75。 1/0.75 = 1.3333 => 你得到大约 33%。但是,如果您更喜欢小内存占用而不是速度,那么您可以安全地采用更小的数字。哈希表会在需要时自动调整大小。
  • 非常彻底。谢谢你。我一直怀疑我不能依赖于能够使用 100% 的初始容量,并且一直假设 75% 左右的填充率会是这种情况。很高兴确认这是在球场上。出于好奇,是什么让您确定我们无论如何都应该选择素数?
  • 使用素数可以最大限度地减少散列表中的聚类。请参阅:Why is it best to use a prime number as a mod in a hashing function?。但不要高估优化。最近,我使用Sieve of Eratosthenes 计算了最多 1 亿个素数(即使用数组new bool[100_000_000])。在单个线程上,这在我的 PC 上只用了不到一秒钟!
  • 很好奇 Dictionary&lt;TKey, TValue&gt; 采用最接近的拟合素数,但 ConcurrentDictionary&lt;TKey, TValue&gt; 没有...特别是考虑到文档没有指示您选择素数。对于毫无戒心的开发人员来说,这可能是一个性能陷阱。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-08-20
  • 2012-08-29
  • 2011-05-10
  • 1970-01-01
  • 1970-01-01
  • 2020-03-29
  • 2012-03-29
相关资源
最近更新 更多