C#添加元素时字典性能不佳

时间:2015-07-22 12:37:08

标签: c# list dictionary

我有大量的数据包含~150万条目。每个条目都是这样一个类的实例:

public class Element
{
    public Guid ID { get; set; }
    public string name { get; set; }
    public property p... p1... p2... 
}

我有一个Guids列表(约4百万),我需要根据Element类的实例列表获取名称。

我将Element对象存储在Dictionary中,但填充数据需要大约90秒。在向字典中添加项目时,有什么方法可以提高性能吗?数据没有重复,但我知道字典在添加新项目时会检查重复项。

如果结构更好,结构不需要是字典。我尝试将Element对象放在List中,在添加时表现更好(~9秒)。但是当我需要用一定的Guid寻找物品时,需要超过10分钟才能找到所有400万个元素。我尝试使用List.Find()并手动迭代列表。

此外,如果不是使用System.Guid,我将它们全部转换为String并将它们的字符串表示存储在数据结构上,整个填充字典和填充其他列表上的名称的操作只需要10秒,但之后我的当我将它们存储为System.Guid时,应用程序消耗1.2Gb的RAM,而不是600mb。

有关如何更好地执行此操作的任何想法?

3 个答案:

答案 0 :(得分:7)

您的问题可能与“顺序”Guid有关,例如:

c482fbe1-9f16-4ae9-a05c-383478ec9d11
c482fbe1-9f16-4ae9-a05c-383478ec9d12
c482fbe1-9f16-4ae9-a05c-383478ec9d13
c482fbe1-9f16-4ae9-a05c-383478ec9d14
c482fbe1-9f16-4ae9-a05c-383478ec9d15

Dictionary<,>存在问题,因为它们通常具有相同的GetHashCode(),因此必须执行一些操作,将搜索时间从O(1)更改为O(n)您可以使用自定义相等比较器来解决它,该比较器以不同的方式计算哈希值,例如:

public class ReverseGuidEqualityComparer : IEqualityComparer<Guid>
{
    public static readonly ReverseGuidEqualityComparer Default = new ReverseGuidEqualityComparer();

    #region IEqualityComparer<Guid> Members

    public bool Equals(Guid x, Guid y)
    {
        return x.Equals(y);
    }

    public int GetHashCode(Guid obj)
    {
        var bytes = obj.ToByteArray();

        uint hash1 = (uint)bytes[0] | ((uint)bytes[1] << 8) | ((uint)bytes[2] << 16) | ((uint)bytes[3] << 24);
        uint hash2 = (uint)bytes[4] | ((uint)bytes[5] << 8) | ((uint)bytes[6] << 16) | ((uint)bytes[7] << 24);
        uint hash3 = (uint)bytes[8] | ((uint)bytes[9] << 8) | ((uint)bytes[10] << 16) | ((uint)bytes[11] << 24);
        uint hash4 = (uint)bytes[12] | ((uint)bytes[13] << 8) | ((uint)bytes[14] << 16) | ((uint)bytes[15] << 24);

        int hash = 37;

        unchecked
        {
            hash = hash * 23 + (int)hash1;
            hash = hash * 23 + (int)hash2;
            hash = hash * 23 + (int)hash3;
            hash = hash * 23 + (int)hash4;
        }

        return hash;
    }

    #endregion
}

然后你只需要像这样声明字典:

var dict = new Dictionary<Guid, Element>(ReverseGuidEqualityComparer.Default);

进行一些测试,看看差异:

private static void Increment(byte[] x)
{
    for (int i = x.Length - 1; i >= 0; i--)
    {
        if (x[i] != 0xFF)
        {
            x[i]++;
            return;
        }

        x[i] = 0;
    }
}

// You can try timing this program with the default GetHashCode:
//var dict = new Dictionary<Guid, object>();
var dict = new Dictionary<Guid, object>(ReverseGuidEqualityComparer.Default);
var hs1 = new HashSet<int>();
var hs2 = new HashSet<int>();

{
    var guid = Guid.NewGuid();

    Stopwatch sw = Stopwatch.StartNew();

    for (int i = 0; i < 1500000; i++)
    {
        hs1.Add(ReverseGuidEqualityComparer.Default.GetHashCode(guid));
        hs2.Add(guid.GetHashCode());
        dict.Add(guid, new object());
        var bytes = guid.ToByteArray();
        Increment(bytes);
        guid = new Guid(bytes);
    }

    sw.Stop();

    Console.WriteLine("Milliseconds: {0}", sw.ElapsedMilliseconds);
}

Console.WriteLine("ReverseGuidEqualityComparer distinct hashes: {0}", hs1.Count);
Console.WriteLine("Guid.GetHashCode() distinct hashes: {0}", hs2.Count);

对于顺序Guid,不同哈希码的数量差异是惊人的:

ReverseGuidEqualityComparer distinct hashes: 1500000
Guid.GetHashCode() distinct hashes: 256

现在......如果你不想使用ToByteArray()(因为它分配了无用的内存),有一个使用反射和表达式树的解决方案...它应该可以正常使用Mono,因为Mono将Guid的实施“public class ReverseGuidEqualityComparer : IEqualityComparer<Guid> { public static readonly ReverseGuidEqualityComparer Default = new ReverseGuidEqualityComparer(); public static readonly Func<Guid, int> GetHashCodeFunc; static ReverseGuidEqualityComparer() { var par = Expression.Parameter(typeof(Guid)); var hash = Expression.Variable(typeof(int)); var const23 = Expression.Constant(23); var const8 = Expression.Constant(8); var const16 = Expression.Constant(16); var const24 = Expression.Constant(24); var b = Expression.Convert(Expression.Convert(Expression.Field(par, "_b"), typeof(ushort)), typeof(uint)); var c = Expression.Convert(Expression.Convert(Expression.Field(par, "_c"), typeof(ushort)), typeof(uint)); var d = Expression.Convert(Expression.Field(par, "_d"), typeof(uint)); var e = Expression.Convert(Expression.Field(par, "_e"), typeof(uint)); var f = Expression.Convert(Expression.Field(par, "_f"), typeof(uint)); var g = Expression.Convert(Expression.Field(par, "_g"), typeof(uint)); var h = Expression.Convert(Expression.Field(par, "_h"), typeof(uint)); var i = Expression.Convert(Expression.Field(par, "_i"), typeof(uint)); var j = Expression.Convert(Expression.Field(par, "_j"), typeof(uint)); var k = Expression.Convert(Expression.Field(par, "_k"), typeof(uint)); var sc = Expression.LeftShift(c, const16); var se = Expression.LeftShift(e, const8); var sf = Expression.LeftShift(f, const16); var sg = Expression.LeftShift(g, const24); var si = Expression.LeftShift(i, const8); var sj = Expression.LeftShift(j, const16); var sk = Expression.LeftShift(k, const24); var body = Expression.Block(new[] { hash }, new Expression[] { Expression.Assign(hash, Expression.Constant(37)), Expression.MultiplyAssign(hash, const23), Expression.AddAssign(hash, Expression.Field(par, "_a")), Expression.MultiplyAssign(hash, const23), Expression.AddAssign(hash, Expression.Convert(Expression.Or(b, sc), typeof(int))), Expression.MultiplyAssign(hash, const23), Expression.AddAssign(hash, Expression.Convert(Expression.Or(d, Expression.Or(se, Expression.Or(sf, sg))), typeof(int))), Expression.MultiplyAssign(hash, const23), Expression.AddAssign(hash, Expression.Convert(Expression.Or(h, Expression.Or(si, Expression.Or(sj, sk))), typeof(int))), hash }); GetHashCodeFunc = Expression.Lambda<Func<Guid, int>>(body, par).Compile(); } #region IEqualityComparer<Guid> Members public bool Equals(Guid x, Guid y) { return x.Equals(y); } public int GetHashCode(Guid obj) { return GetHashCodeFunc(obj); } #endregion // For comparison purpose, not used public int GetHashCodeSimple(Guid obj) { var bytes = obj.ToByteArray(); unchecked { int hash = 37; hash = hash * 23 + (int)((uint)bytes[0] | ((uint)bytes[1] << 8) | ((uint)bytes[2] << 16) | ((uint)bytes[3] << 24)); hash = hash * 23 + (int)((uint)bytes[4] | ((uint)bytes[5] << 8) | ((uint)bytes[6] << 16) | ((uint)bytes[7] << 24)); hash = hash * 23 + (int)((uint)bytes[8] | ((uint)bytes[9] << 8) | ((uint)bytes[10] << 16) | ((uint)bytes[11] << 24)); hash = hash * 23 + (int)((uint)bytes[12] | ((uint)bytes[13] << 8) | ((uint)bytes[14] << 16) | ((uint)bytes[15] << 24)); return hash; } } } ”与2004中的微软实现“对齐”,这是古代: - )

public class ReverseGuidEqualityComparer : IEqualityComparer<Guid>
{
    public static readonly ReverseGuidEqualityComparer Default = new ReverseGuidEqualityComparer();

    #region IEqualityComparer<Guid> Members

    public bool Equals(Guid x, Guid y)
    {
        return x.Equals(y);
    }

    public int GetHashCode(Guid obj)
    {
        GuidToInt32 gtoi = new GuidToInt32 { Guid = obj };

        unchecked
        {
            int hash = 37;

            hash = hash * 23 + gtoi.Int32A;
            hash = hash * 23 + gtoi.Int32B;
            hash = hash * 23 + gtoi.Int32C;
            hash = hash * 23 + gtoi.Int32D;

            return hash;
        }
    }

    #endregion

    [StructLayout(LayoutKind.Explicit)]
    private struct GuidToInt32
    {
        [FieldOffset(0)]
        public Guid Guid;

        [FieldOffset(0)]
        public int Int32A;
        [FieldOffset(4)]
        public int Int32B;
        [FieldOffset(8)]
        public int Int32C;
        [FieldOffset(12)]
        public int Int32D;
    }
}

其他解决方案,基于“未记录但工作”的编程(在.NET和Mono上测试):

StructLayout

它使用Guid“技巧”将int添加到一堆Guid,写入int并阅读GetHashCode()。< / p>

为什么Guid.GetHashCode()出现连续ID问题?

很容易解释:从reference source开始,return _a ^ (((int)_b << 16) | (int)(ushort)_c) ^ (((int)_f << 24) | _k); 是:

_d

所以_e_g_h_i_jbyte Guid不属于哈希码。递增时_k首先在_j字段中递增(256个值),然后在_i字段溢出(256 * 256个值,所以65536个值),然后在{ {1}}字段(16777216个值)。显然,如果不对_h_i_j字段进行哈希处理,则序列Guid的哈希值将仅显示非大范围Guid的256个不同值(或者,如果_f字段增加一次,最多可以有512个不同的值,例如,如果您的Guid类似于12345678-1234-1234-1234-aaffffffff00,那么aa(即“{1}}在_f的256个增量后,“ab”将增加到Guid

答案 1 :(得分:4)

  

我不是,字典Key是Element类的ID属性,而不是Element类本身。此属性的类型为System.Guid。

Guid的问题在于它是一个非常专业的构造。首先,它是struct,而不是class。移动这个东西不像移动指针那么简单(技术上是一个句柄,但同样的事情),它涉及移动整个内存块。请记住,.NET内存模型使一切都变得紧凑,因此还需要移动其他块来腾出空间。

同时查看source code,它将所有部分存储为单独的字段,其中11个!这是150万条目的大量比较。

我要做的是创建一种替代的Guid实施(class,而不是struct!),以便进行有效的比较。不需要所有花哨的解析,只关注速度。 Guids的长度为16个字节,这意味着4个long字段。您需要照常实现Equals(比较4个字段)和GetHashCode,例如对字段进行异或。我确信这很好。

编辑:请注意,我并不是说框架提供的实现很糟糕,它只是没有为你尝试用它做什么。事实上,这对你的目的来说很糟糕。

答案 2 :(得分:2)

如果您的数据已预先排序,您可以使用List<T>.BinarySearch快速搜索列表。您需要创建一个比较器类,并使用它来查找。

class ElementComparer : IComparer<Element>
{
    public int Compare(Element x, Element y)
    {
        // assume x and y is not null
        return x.ID.CompareTo(y.ID);
    }
}

然后使用它

var comparer = new ElementComparer();
var elements = new List<Element>(1500000); // specify capacity might help a bit

//... (load your list here. Sort it with elements.Sort(comparer) if needed)

Guid guid = elements[20]; // for the sake of testing
int idx = elements.BinarySearch(new Element { ID = guid }, comparer);

如果你愿意的话,你可以将这整件事包装在IReadOnlyDictionary<Guid, Element>中,但也许你不需要这种情况。