C# 把一本大词典分成多个部分是明智的吗?
我用字典来存放大量(>10^7)的条目。如果它被拆分成多个单独的字典,每个字典都保存一部分/分区的数据,它能提高查找和/或插入性能吗 举个例子,假设我们有一本C# 把一本大词典分成多个部分是明智的吗?,c#,.net,performance,dictionary,C#,.net,Performance,Dictionary,我用字典来存放大量(>10^7)的条目。如果它被拆分成多个单独的字典,每个字典都保存一部分/分区的数据,它能提高查找和/或插入性能吗 举个例子,假设我们有一本字典。我们可以将其替换为: var ds = new Dictionary<int,int> [256]; // ... void Add (int key, int value) { // We can assume key is an evenly distributed hash ds[key &
字典
。我们可以将其替换为:
var ds = new Dictionary<int,int> [256];
// ...
void Add (int key, int value) {
// We can assume key is an evenly distributed hash
ds[key & 0xFF].Add (key, value);
}
// Lookup similar
var-ds=新字典[256];
// ...
无效添加(整型键,整型值){
//我们可以假设密钥是均匀分布的散列
ds[key&0xFF]。添加(key,value);
}
//查找相似
当然,这需要进行基准测试,但我也对这种情况下的一般建议感兴趣。令人惊讶的是,我在这里找不到真正类似的问题
我知道一本词典所能容纳的词条数量是有限的。这个问题假设这个限制不是问题——否则,无论如何只有一个解决方案。我已经考虑了更多。虽然许多数据结构显示插入或查找操作的对数成本,但在字典中,这些成本(摊销)假定为O(1) 在前一种情况下,通过手动索引(O(1)操作)拆分一部分功可以通过减少对数参数来减少剩余功。实际上,我们将在另一个结构之上实现一个字典 当然,这也意味着,当基本结构本身已经是一个字典时,这不应该有任何显著的影响。有许多方法可以实现这些功能,但据我所知,没有一种方法可以通过减小它们的大小而逐渐受益:它们的平均案例行为(即处理重复项)在时间上是恒定的,并且不会增长 另一方面,手工工作会带来开销。因此,我们预计这样一个切片字典的性能会更差 为了检查这一点,我写了一个小测试
Console.WriteLine ("Times in seconds per 10m merged/sliced operations");
foreach (var init in new[] { "empty", "size", "spare" }) {
for (int n = 10 * 1000 * 1000; n <= 40 * 1000 * 1000; n += 10 * 1000 * 1000) {
for (int repeat = 0; repeat < 3; repeat++) {
Stopwatch wmi, wml, wsi, wsl;
{
GC.Collect ();
var r = new Random (0);
Dictionary<int, object> d;
if (init == "empty") {
d = new Dictionary<int, object> ();
}
else if (init == "size") {
d = new Dictionary<int, object> (n);
}
else {
d = new Dictionary<int, object> (2 * n);
}
wmi = Stopwatch.StartNew ();
for (int i = 0; i < n; i++) {
var key = r.Next ();
d[key] = null;
}
wmi.Stop ();
wml = Stopwatch.StartNew ();
var dummy = false;
for (int i = 0; i < n; i++) {
dummy ^= d.ContainsKey (i);
}
wml.Stop ();
}
{
GC.Collect ();
var r = new Random (0);
var ds = new Dictionary<int, object>[256];
for (int i = 0; i < 256; i++) {
if (init == "empty") {
ds[i] = new Dictionary<int, object> ();
}
else if (init == "size") {
ds[i] = new Dictionary<int, object> (n / 256);
}
else {
ds[i] = new Dictionary<int, object> (2 * n / 256);
}
}
wsi = Stopwatch.StartNew ();
for (int i = 0; i < n; i++) {
var key = r.Next ();
var d = unchecked(ds[key & 0xFF]);
d[key] = null;
}
wsi.Stop ();
wsl = Stopwatch.StartNew ();
var dummy = false;
for (int i = 0; i < n; i++) {
var d = unchecked(ds[i & 0xFF]);
dummy ^= d.ContainsKey (i);
}
wsl.Stop ();
}
string perM (Stopwatch w) => $"{w.Elapsed.TotalSeconds / n * 10 * 1000 * 1000,5:0.00}";
Console.WriteLine ($"init = {init,-5}, n = {n,8};"
+ $" insert = {perM (wmi)}/{perM (wsi)},"
+ $" lookup = {perM (wml)}/{perM (wsl)}");
}
}
Console.WriteLine ();
}
一致地,在切片字典中插入和查找都不会更快。我相信在大多数情况下都是这样
然而,在并行操作中仍然存在这样一个切片字典的可能用例。字典的不同部分可以并行处理批处理操作数据的插入、查找等
无论字典作为一个整体是否同时使用,这都是正确的。但是,如果是,那么切片将只允许锁定所需的部分,而不是所有部分(在一个幼稚的实现中)。但是,其他为从头开始的并发操作而设计的字典(例如.NET的
ConcurrentDictionary
)可以免于这一缺点。将字典保持在一个整体中将为您提供最佳的平均性能,但每次需要它的内部数组时,您都会获得巨大的点击率
// Create the dictionary
var dict = new Dictionary<int, int>(19998337); // 90 msec
// Populate the dictionary
for (int i = 0; i < 19998337; i++) dict.Add(i, i); // 850 msec
// Add one more entry that requires resize
dict.Add(-1, -1); // 850 msec
这不要紧-一个.NET字典已经实现为一个散列容器,所以您的拆分只是做第二个散列来细分为其他散列容器。除非你真的幸运地使用第一个散列函数来分发数据,否则(乍一看)你提出的解决方案并不比仅仅使用字典好。要判断拥有多个字典是否更好,唯一的方法就是,正如你所建议的,自己对它进行基准测试。我们不知道你现在是如何输入字典的,所以我们没什么可以帮你的。如果您有一种细分数据的方法,它可能会有所帮助。不过,有这么多的项目,我可能会建议使用第三方服务,比如Elasticsearch。根据mi测试,32位的48*10^6项目和64位的96*10^6项目在内存中崩溃。所以这是一个严格的理论问题。我认为提高的机会在更高的层次上-你需要这个做什么?@AntonínLejsek很好,错别字减少了一个数量级我需要一个便宜的内存KVS,以便在我正在处理的一些相当大的统计计算中进行批处理。但问题的范围确实更广。如果您将主要哈希函数更改为
ds[(uint)i>>24]
,则会有一个可测量的差异。这可能是因为它改进了测试中的内存局部性。问题是,真实案例的测试代表性是多少。这一点很好,但我完全没有谈到这一点。为了解释您的想法:在回答中,我默默地使用了几乎最坏的内存位置进行查找:由于索引&0xFF
,几乎每个项都引用了不同的子字典。另一方面,将其更改为索引>>24
是最好的情况,因为后续索引引用相同的切片。显然,后一种情况很有可能更快。衡量这一点,我的结果与你的一致:速度明显加快(我的盒子上的系数为1.5到2.5)。因此,如果可以假设内存的局部性,那么切片可能是有用的,因为每次调整大小都会使大小增加一倍。这意味着,在调整大小时复制的项目数大致等于字典的实际容量。一个大字典和多个小字典的总调整开销大致相同。@AntonínLejsek true。但是如果你正在做一些实时的事情,比如流式传输视频帧,你可能希望避免这些不常见的大打嗝。
// Create the dictionary
var dict = new Dictionary<int, int>(19998337); // 90 msec
// Populate the dictionary
for (int i = 0; i < 19998337; i++) dict.Add(i, i); // 850 msec
// Add one more entry that requires resize
dict.Add(-1, -1); // 850 msec
public class SegmentedDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
private class CachedComparer : IEqualityComparer<TKey>
{
private readonly IEqualityComparer<TKey> _source;
private int? _cachedHashCode;
public CachedComparer(IEqualityComparer<TKey> source)
{
_source = source ?? EqualityComparer<TKey>.Default;
}
public bool Equals(TKey x, TKey y) => _source.Equals(x, y);
public int GetHashCodeAndCache(TKey key)
{
int hashCode = _source.GetHashCode(key);
_cachedHashCode = hashCode;
return hashCode;
}
public int GetHashCode(TKey key)
{
if (_cachedHashCode.HasValue)
{
int hashCode = _cachedHashCode.Value;
_cachedHashCode = null; // Use the cache only once
return hashCode;
}
return _source.GetHashCode(key);
}
}
private readonly CachedComparer _comparer;
private readonly Dictionary<TKey, TValue>[] _segments;
public SegmentedDictionary(int segmentsCount, int capacityPerSegment,
IEqualityComparer<TKey> comparer)
{
_comparer = new CachedComparer(comparer);
_segments = new Dictionary<TKey, TValue>[segmentsCount];
for (int i = 0; i < segmentsCount; i++)
{
_segments[i] = new Dictionary<TKey, TValue>(
capacityPerSegment, _comparer);
}
}
private Dictionary<TKey, TValue> GetSegment(TKey key)
{
var hashCode = _comparer.GetHashCodeAndCache(key);
var index = Math.Abs(hashCode) % _segments.Length;
return _segments[index];
}
public int Count => _segments.Sum(d => d.Count);
public TValue this[TKey key]
{
get => GetSegment(key)[key];
set => GetSegment(key)[key] = value;
}
public void Add(TKey key, TValue value) => GetSegment(key).Add(key, value);
public bool ContainsKey(TKey key) => GetSegment(key).ContainsKey(key);
public bool TryGetValue(TKey key, out TValue value)
=> GetSegment(key).TryGetValue(key, out value);
public bool Remove(TKey key) => GetSegment(key).Remove(key);
public void Clear() => Array.ForEach(_segments, d => d.Clear());
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
=> _segments.SelectMany(d => d).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
ICollection<TKey> IDictionary<TKey, TValue>.Keys
=> throw new NotImplementedException();
ICollection<TValue> IDictionary<TKey, TValue>.Values
=> throw new NotImplementedException();
void ICollection<KeyValuePair<TKey, TValue>>.Add(
KeyValuePair<TKey, TValue> item)
=> throw new NotImplementedException();
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(
KeyValuePair<TKey, TValue> item)
=> throw new NotImplementedException();
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(
KeyValuePair<TKey, TValue>[] array, int arrayIndex)
=> throw new NotImplementedException();
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(
KeyValuePair<TKey, TValue> item)
=> throw new NotImplementedException();
bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
=> throw new NotImplementedException();
}