计算.Net BitArray类中设置的位

时间:2011-02-21 06:59:18

标签: .net algorithm bitarray

我正在实现一个库,我广泛使用.Net BitArray类,需要等效于Java BitSet.Cardinality()方法,即返回设置位数的方法。我正在考虑将其实现为BitArray类的扩展方法。平凡的实现是迭代和计数位集(如下所示),但我想要更快的实现,因为我将执行数千个集合操作并计算答案。有比下面的例子更快的方法吗?

count = 0;

for (int i = 0; i < mybitarray.Length; i++)
{

  if (mybitarray [i])
    count++;
}

11 个答案:

答案 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查找之间的区别如下:
阵列:

  1. offset = index * indexSize
  2. 获取位置+偏移的内存并保存到值
  3. BitArray:

    1. index = index / indexSize
    2. offset = index * indexSize
    3. 获取位置+偏移的内存并保存到值
    4. position = index%indexSize
    5. 移位值位置位
    6. value = value and 1
    7. 除阵列#2和#3外,大多数这些命令需要1个处理器周期才能完成。有些命令可以使用x86 / x64处理器组合成1个命令,但可能不使用ARM,因为它使用的是一组简化的指令。
      两者中的哪一个(数组或BitArray)表现更好将特定于您的平台(处理器速度,处理器指令,处理器缓存大小,处理器缓存速度,系统内存量(Ram),系统内存速度(CAS),速度处理器和RAM之间的连接)以及要计算的索引的扩展(最常聚集的交叉点或随机分布的交叉点)。

      总结一下:你可能会找到一种方法让它更快,但你的解决方案是你在.NET中使用每个布尔模型获得的数据集最快的。

      编辑确保您正在按顺序访问要计算的索引。如果按顺序访问索引200,5,150,151,311,6,则会增加缓存未命中量,从而导致花费更多时间等待从RAM检索值。

答案 10 :(得分:0)

我有同样的问题,但不仅仅有一个Cardinality方法可以转换。所以,我选择移植整个BitSet类。幸运的是它是独立的。

这是the Gist of the C# port

如果有人报告发现的任何错误,我将不胜感激 - 我不是Java开发人员,并且对位逻辑的经验有限,所以我可能错误地翻译了一些错误。