我正在实现一个库,我广泛使用.Net BitArray类,需要等效于Java BitSet.Cardinality()方法,即返回设置位数的方法。我正在考虑将其实现为BitArray类的扩展方法。平凡的实现是迭代和计数位集(如下所示),但我想要更快的实现,因为我将执行数千个集合操作并计算答案。有比下面的例子更快的方法吗?
count = 0;
for (int i = 0; i < mybitarray.Length; i++)
{
if (mybitarray [i])
count++;
}
答案 0 :(得分:29)
这是我的解决方案,基于http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
中的“最佳位计数方法”public static Int32 GetCardinality(BitArray bitArray)
{
Int32[] ints = new Int32[(bitArray.Count >> 5) + 1];
bitArray.CopyTo(ints, 0);
Int32 count = 0;
// fix for not truncated bits in last integer that may have been set to true with SetAll()
ints[ints.Length - 1] &= ~(-1 << (bitArray.Count % 32));
for (Int32 i = 0; i < ints.Length; i++)
{
Int32 c = ints[i];
// magic (http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel)
unchecked
{
c = c - ((c >> 1) & 0x55555555);
c = (c & 0x33333333) + ((c >> 2) & 0x33333333);
c = ((c + (c >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
}
count += c;
}
return count;
}
根据我的测试,这比简单的foreach循环快约60倍,仍然比Kernighan方法快30倍,在具有1000位的BitArray中将大约50%的位设置为true。如果需要,我也有VB版本。
答案 1 :(得分:3)
你可以用Linq轻松完成这个任务
BitArray ba = new BitArray(new[] { true, false, true, false, false });
var numOnes = (from bool m in ba
where m
select m).Count();
答案 2 :(得分:2)
BitArray myBitArray = new BitArray(...
int
bits = myBitArray.Count,
size = ((bits - 1) >> 3) + 1,
counter = 0,
x,
c;
byte[] buffer = new byte[size];
myBitArray.CopyTo(buffer, 0);
for (x = 0; x < size; x++)
for (c = 0; buffer[x] > 0; buffer[x] >>= 1)
counter += buffer[x] & 1;
取自“Counting bits set, Brian Kernighan's way”并改编为字节。我正在将它用于1 000 000+位的位数组,而且它非常棒 如果你的位不是n * 8,那么你可以手动计算mod字节。
答案 3 :(得分:1)
你可以使用Linq,但它会变得无用而且速度慢:
var sum = mybitarray.OfType<bool>().Count(p => p);
答案 4 :(得分:1)
使用BitArray
没有更快的方法 - 它归结为你需要计算它们 - 你可以使用LINQ来做或者做你自己的循环,但是没有提供的方法{ {1}}和底层数据结构是一个BitArray
数组(如Reflector所示) - 所以这总是O(n),n是数组中的位数。
我能想到让它变得更快的唯一方法是使用反射来获取基础int[]
字段,然后你可以绕过m_array
在每次调用时使用的边界检查(参见下面) - 但这有点脏,可能只在非常大的阵列上值得,因为反射很昂贵。
Get()
如果这个优化对你来说非常重要,你应该创建自己的位操作类,在内部可以使用public bool Get(int index)
{
if ((index < 0) || (index >= this.Length))
{
throw new ArgumentOutOfRangeException("index", Environment.GetResourceString("ArgumentOutOfRange_Index"));
}
return ((this.m_array[index / 0x20] & (((int) 1) << (index % 0x20))) != 0);
}
,但是跟踪设置的位数并提供适当的方法(主要是委托给BitArray
但是添加方法来获取当前设置的位数) - 当然这将是O(1)。
答案 5 :(得分:1)
如果你真的想要最大化速度,你可以预先计算一个查找表,其中给定一个具有基数的字节值,但BitArray不是最理想的结构,因为你需要使用反射将基础存储从中拉出并对整数类型进行操作 - 请参阅this question以获得对该技术的更好解释。
另一种可能更有用的技术是使用类似the Kernighan trick的东西,对于基数m的n位值,它是O(m)。
static readonly ZERO = new BitArray (0);
static readonly NOT_ONE = new BitArray (1).Not ();
public static int GetCardinality (this BitArray bits)
{
int c = 0;
var tmp = new BitArray (myBitArray);
for (c; tmp != ZERO; c++)
tmp = tmp.And (tmp.And (NOT_ONE));
return c;
}
这也比C语言更麻烦,因为在整数类型和BitArrays之间没有定义操作,(例如,tmp &= tmp - 1
用于清除最不重要的设置位,已经翻译为tmp &= (tmp & ~0x1)
。
我不知道这对于BCL BitArray的情况是否比天真迭代更快,但从算法来说它应该更优越。
编辑:引用我发现Kernighan技巧的地方,有更深入的解释
答案 6 :(得分:1)
如果您不介意将System.Collections.BitArray的代码复制到您的项目并编辑它,您可以写为: (我认为它是最快的。我尝试使用BitVector32 []来实现我的BitArray,但它仍然很慢。)
public void Set(int index, bool value)
{
if ((index < 0) || (index >= this.m_length))
{
throw new ArgumentOutOfRangeException("index", "Index Out Of Range");
}
SetWithOutAuth(index,value);
}
//When in batch setting values,we need one method that won't auth the index range
private void SetWithOutAuth(int index, bool value)
{
int v = ((int)1) << (index % 0x20);
index = index / 0x20;
bool NotSet = (this.m_array[index] & v) == 0;
if (value && NotSet)
{
CountOfTrue++;//Count the True values
this.m_array[index] |= v;
}
else if (!value && !NotSet)
{
CountOfTrue--;//Count the True values
this.m_array[index] &= ~v;
}
else
return;
this._version++;
}
public int CountOfTrue { get; internal set; }
public void BatchSet(int start, int length, bool value)
{
if (start < 0 || start >= this.m_length || length <= 0)
return;
for (int i = start; i < length && i < this.m_length; i++)
{
SetWithOutAuth(i,value);
}
}
答案 7 :(得分:1)
我没有找到使用查找表的版本后写了我的版本:
private int[] _bitCountLookup;
private void InitLookupTable()
{
_bitCountLookup = new int[256];
for (var byteValue = 0; byteValue < 256; byteValue++)
{
var count = 0;
for (var bitIndex = 0; bitIndex < 8; bitIndex++)
{
count += (byteValue >> bitIndex) & 1;
}
_bitCountLookup[byteValue] = count;
}
}
private int CountSetBits(BitArray bitArray)
{
var result = 0;
var numberOfFullBytes = bitArray.Length / 8;
var numberOfTailBits = bitArray.Length % 8;
var tailByte = numberOfTailBits > 0 ? 1 : 0;
var bitArrayInBytes = new byte[numberOfFullBytes + tailByte];
bitArray.CopyTo(bitArrayInBytes, 0);
for (var i = 0; i < numberOfFullBytes; i++)
{
result += _bitCountLookup[bitArrayInBytes[i]];
}
for (var i = (numberOfFullBytes * 8); i < bitArray.Length; i++)
{
if (bitArray[i])
{
result++;
}
}
return result;
}
答案 8 :(得分:1)
由于使用了 System.Numerics.BitOperations.PopCount
C#
Int32[] ints = new Int32[(bitArray.Count >> 5) + 1];
bitArray.CopyTo(ints, 0);
Int32 count = 0;
for (Int32 i = 0; i < ints.Length; i++) {
count += BitOperations.PopCount(ints[i]);
}
Console.WriteLine(count);
F#
let ints = Array.create ((bitArray.Count >>> 5) + 1) 0u
bitArray.CopyTo(ints, 0)
ints
|> Array.sumBy BitOperations.PopCount
|> printfn "%d"
在Is BitOperations.PopCount the best way to compute the BitArray cardinality in .NET?中查看更多详情
答案 9 :(得分:0)
问题自然是O(n),因此您的解决方案可能效率最高。
由于您正在尝试计算任意位的子集,因此在设置它们时无法对这些位进行计数(如果您不经常设置位,则会提供速度提升)。
您可以检查您使用的处理器是否具有将返回设置位数的命令。例如,具有SSE4的处理器可以使用POPCNT according to this post。这可能对你不起作用,因为.Net不允许汇编(因为它与平台无关)。此外,ARM处理器可能没有等效的。
最好的解决方案可能是查找表(如果可以保证交换机将编译为单个跳转到currentLocation + byteValue,则可以切换)。这将为您提供整个字节的计数。当然,BitArray不允许访问底层数据类型,因此您必须创建自己的BitArray。您还必须保证字节中的所有位始终是交叉点的一部分,这听起来不太可能。
另一种选择是使用布尔数组而不是BitArray。这样做的优点是不需要从字节中的其他位中提取该位。缺点是阵列将占用内存空间的8倍,这意味着不仅浪费了空间,而且在迭代数组以执行计数时还会有更多的数据推送。
标准数组查找和BitArray查找之间的区别如下:
阵列:
BitArray:
除阵列#2和#3外,大多数这些命令需要1个处理器周期才能完成。有些命令可以使用x86 / x64处理器组合成1个命令,但可能不使用ARM,因为它使用的是一组简化的指令。
两者中的哪一个(数组或BitArray)表现更好将特定于您的平台(处理器速度,处理器指令,处理器缓存大小,处理器缓存速度,系统内存量(Ram),系统内存速度(CAS),速度处理器和RAM之间的连接)以及要计算的索引的扩展(最常聚集的交叉点或随机分布的交叉点)。
总结一下:你可能会找到一种方法让它更快,但你的解决方案是你在.NET中使用每个布尔模型获得的数据集最快的。
编辑确保您正在按顺序访问要计算的索引。如果按顺序访问索引200,5,150,151,311,6,则会增加缓存未命中量,从而导致花费更多时间等待从RAM检索值。
答案 10 :(得分:0)
我有同样的问题,但不仅仅有一个Cardinality方法可以转换。所以,我选择移植整个BitSet类。幸运的是它是独立的。
如果有人报告发现的任何错误,我将不胜感激 - 我不是Java开发人员,并且对位逻辑的经验有限,所以我可能错误地翻译了一些错误。