我得到了一个无序列表。在80到140个项目之间,每个项目的值在0到175之间。
我正在生成该列表的列表,其中大约有5到1000万。
我需要尽快处理所有唯一有序的序列(不包括重复项)。
我现在的操作方式是创建列表所有值的哈希,然后将其插入到哈希集中。
分析时的两个热点是ToArray() HOTSPOT1 和Array.Sort() HOTSPOT2
是否有更好的方法来完成该任务,或者有更好的替代方法来修复这两个热点?速度很重要。
小型演示,我尝试尽可能多地复制
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApp1
{
class Example
{
//some other properties
public int Id { get; set; }
}
class Program
{
static void Main(string[] args)
{
var checkedUnlock = new HashSet<int>();
var data = FakeData();
foreach (List<Example> subList in data)
{
var hash = CalcHash(subList.Select(x => x.Id).ToArray()); // HOTPSOT1
var newHash = checkedUnlock.Add(hash);
if (newHash)
{
//do something
}
}
}
static int CalcHash(int[] value)
{
Array.Sort(value); // HOTPSOT2
int hash;
unchecked // https://stackoverflow.com/a/263416/40868
{
hash = (int)2166136261;
var i = value.Length;
while (i-- > 0)
hash = (hash * 16777619) ^ value[i];
}
return hash;
}
//don't look at this, this is just to fake data
static List<List<Example>> FakeData()
{
var data = new List<List<Example>>();
var jMax = 10; //normally between 80 and 140
var idMax = 25; //normally between 0 and 175
var rnd = new Random(42);
var ids = Enumerable.Range(0, idMax).ToArray();
for (int i = 0; i < 500000; ++i)
{
//force duplicate
if(i % 50000 == 0)
{
ids = Enumerable.Range(0, idMax).ToArray();
rnd = new Random(42);
}
for (int r = 0; r < idMax; ++r)
{
int randomIndex = rnd.Next(idMax);
int temp = ids[randomIndex];
ids[randomIndex] = ids[r];
ids[r] = temp;
}
var subList = new List<Example>();
data.Add(subList);
for (int j = 0; j < jMax; ++j)
{
subList.Add(new Example() { Id = ids[j] });
}
}
return data;
}
}
}
答案 0 :(得分:3)
因此,您有一个最多可以包含140个项目的数组,并且所有值都在0到175之间。数组中的所有值都是唯一的,顺序无关紧要。也就是说,数组[20, 90, 16]
将被视为与[16, 20, 90]
相同。
鉴于此,您可以将一个数组表示为一组175位。更好的是,您可以创建集合而不必对输入数组进行排序。
您将C#中的集合表示为BitArray。要计算数组的哈希码,请创建集合,然后遍历集合以获取哈希码。看起来像这样:
private BitArray HashCalcSet = new BitArray(175);
int CalcHash(int[] a, int startIndex)
{
// construct the set
HashCalcSet.SetAll(false);
for (var i = startIndex; i < a.Length; ++i)
{
HashCalcSet[a[i]] = true;
}
// compute the hash
hash = (int)2166136261;
for (var i = 174; i >= 0; --i)
{
if (HashCalcSet[i])
{
hash = (hash * 16777619) ^ value[i];
}
}
return hash;
}
这消除了排序和ToArray
。您必须在BitArray
上循环几次,但是在BitArray
上进行三遍传递可能比排序更快。
我对您的解决方案看到的一个问题是您如何使用HashSet
。您有以下代码:
var hash = CalcHash(subList.Select(x => x.Id).ToArray()); // HOTPSOT1
var newHash = checkedUnlock.Add(hash);
if (newHash)
{
//do something
}
该代码错误地假设,如果两个数组的哈希码相等,则数组相等。您正在生成一个175位数量的32位哈希码。肯定会有哈希冲突。您最终会说,两个数组不同时,它们是相同的。
如果您对此感到担心,请告诉我,我可以编辑答案以提供解决方案。
如果您希望能够比较项目是否相等,而不仅仅是检查其哈希码是否相同,则需要创建一个具有Equals
和GetHashCode
方法的对象。您将把该对象插入HashSet
中。这些对象中最简单的对象将包含上文所述的BitArray
以及对其进行操作的方法。像这样:
class ArrayObject
{
private BitArray theBits;
private int hashCode;
public override bool Equals(object obj)
{
if (object == null || GetType() != obj.GetType())
{
return false;
}
ArrayObject other = (ArrayObject)obj;
// compare two BitArray objects
for (var i = 0; i < theBits.Length; ++i)
{
if (theBits[i] != other.theBits[i])
return false;
}
return true;
}
public override int GetHashCode()
{
return hashCode;
}
public ArrayObject(int hash, BitArray bits)
{
theBits = bits;
hashCode = hash;
}
}
这样的想法是,您按照上述方法构造BitArray
和哈希码(尽管您必须为每个调用分配一个新的BitArray
),然后创建并返回这些ArrayObject
实例之一。
您的HashSet
成为HashSet<ArrayObject>
。
以上方法有效,但它占用了大量内存。您可以通过创建仅包含三个long
整数的类来减少内存需求。您无需直接使用BitArray
,而直接操作这些位。您映射这些位,以便数字0到63修改第一个数字中的位0到63。数字64到127对应于第二个数字的位0到63,以此类推。因此,您不必保存单独的哈希码,因为从三个long中进行计算很容易,并且相等比较变得容易得多也是如此。
该类看起来像这样。理解,我还没有测试代码,但是这个想法应该是正确的。
class ArrayObject2
{
private long l1;
private long l2;
private long l3;
public ArrayObject2(int[] theArray)
{
for (int i = 0; i < theArray.Length; ++i)
{
var rem = theArray[i] % 63;
int bitVal = 1 << rem;
if (rem < 64) l1 |= bitVal;
else if (rem < 128) l2 |= bitVal;
else l3 |= bitVal;
}
}
public override bool Equals(object obj)
{
var other = obj as ArrayObject2;
if (other == null) return false;
return l1 == other.l1 && l2 == other.l2 && l3 == other.l3;
}
public override int GetHashCode()
{
// very simple, and not very good hash function.
return (int)l1;
}
}
正如我在代码中评论的那样,哈希函数并不是很好。它将起作用,但是通过一些研究您可以做得更好。
此方法的优点是使用的内存少于BitArray
或Boolean
数组。它可能会比bool
的数组慢。它可能比BitArray
代码快。但是无论如何,它都可以使您避免错误地假设相同的哈希码等于相同的数组。
答案 1 :(得分:1)
我认为您可以通过重用一个更大的数组来节省一些时间,而不必每次都分配新的数组而导致额外的内存通信和垃圾回收。
这将需要自定义排序实现,该实现知道即使数组可以有1000个项目,但对于当前运行,仅需要对前80个项目进行排序(散列也是如此)。在id的子范围上运行的quicksort看起来应该可以正常工作。快速的想法样本(尚未经过详细测试)
int[] buffer = new int[1000];
foreach (List<Example> subList in data)
{
for (int i = 0; i < subList.Count; i++)
{
buffer[i] = subList[i].Id;
}
var hash = CalcHashEx(buffer, 0, subList.Count - 1);
var newHash = checkedUnlock.Add(hash);
if (newHash)
{
//do something
}
}
public static void QuickSort(int[] elements, int left, int right)
{
int i = left, j = right;
int pivot = elements[(left + right) / 2];
while (i <= j)
{
while (elements[i] < pivot)
{
i++;
}
while (elements[j] > pivot)
{
j--;
}
if (i <= j)
{
// Swap
int tmp = elements[i];
elements[i] = elements[j];
elements[j] = tmp;
i++;
j--;
}
}
if (left < j)
{
QuickSort(elements, left, j);
}
if (i < right)
{
QuickSort(elements, i, right);
}
}
static int CalcHashEx(int[] value, int startIndex, int endIndex)
{
QuickSort(value, startIndex, endIndex);
int hash;
unchecked // https://stackoverflow.com/a/263416/40868
{
hash = (int)2166136261;
var i = endIndex + 1;
while (i-- > 0)
hash = (hash * 16777619) ^ value[i];
}
return hash;
}
答案 2 :(得分:1)
此版本的CalcHash()
可让您删除.ToArray()
并用可以对序列起作用的不同东西替换Array.Sort()
,而不需要整个集合...所以两个热点。
static int CalcHash(IEnumerable<int> value)
{
value = value.OrderByDescending(i => i);
int hash;
unchecked // https://stackoverflow.com/a/263416/40868
{
hash = (int)2166136261;
foreach(var item in value)
{
hash = (hash * 16777619) ^ item;
}
}
return hash;
}
我不确定OrderByDescending()
的表现如何。我怀疑它会比Array.Sort()
慢一些,但由于消除了ToArray()
而仍然是一个全面的胜利...但是您需要再次运行探查器才能确定。
通过.GroupBy()
消除或减少分支,并在每个组的.First()
项目上运行代码,您可能还会得到一些改进:
var groups = data.GroupBy(sub => CalcHash(sub.Select(x => x.Id)));
foreach(List<Example> subList in groups.Select(g => g.First()))
{
//do something
}
答案 3 :(得分:0)
将其放在此处,因为将其放在注释中没有意义
到目前为止,我所做的是创建一个布尔数组并将存在时将该项目的索引设置为true,然后我将CalcHash替换为;
unchecked
{
hash = (int)2166136261;
var i = theMaxLength;
while (i-- > 0)
if(testing[i]) //the array of boolean
{
hash = (hash * 16777619) ^ i;
testing[i] = false;
}
}
这样做是为了彻底删除ToArray()和Array.Sort(),此解决方案是根据dlxeon / jim / joel答案构建的
我将运行时减少了大约20-25%,这很好