【问题标题】:Counting bits set in a .Net BitArray Class计数在 .Net BitArray 类中设置的位
【发布时间】:2011-07-01 02:16:56
【问题描述】:

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

count = 0;

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

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

【问题讨论】:

  • 作为旁注,我将补充一点,从 Mono 中获取 BitArray 代码并添加 O(1) 的基数是新手级编程。 (类库在 X11 许可下,这是一个非常非常宽松的许可)
  • 有趣的建议。源代码不会在 C 中吗?在这种情况下,我需要让我的图书馆不受损坏?也请您指出我在 github 上的正确路径吗?
  • 不不... 95%(它是一个随机数)框架库(和单声道库)是用 C#(纯 C#,而不是 C# + 托管 C++)编写的。只有最低级别的东西是用 C(或其他东西)编写的(我没有注意到你问过我......你(和我,因为 50% 的时间我忘记了)应该记住@name 你想要的人写信给:-))

标签: .net algorithm bitarray


【解决方案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?查看更多详情

【讨论】:

  • 这个答案效果很好,除了 BitOperations.PopCount 需要UInt32,而不是Int32。只需将第一行更改为UInt32,效果很好。
【解决方案2】:

我遇到了同样的问题,但要转换的基数方法不止一种。所以,我选择移植整个 BitSet 类。幸运的是它是独立的。

这里是the Gist of the C# port。

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

【讨论】:

    【解决方案3】:

    我在没有找到使用查找表的版本之后写了我的版本:

    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;
    }
    

    【讨论】:

      【解决方案4】:

      这是我基于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% 的位设置为真。如果需要,我也有这个的 VB 版本。

      【讨论】:

      • 谢谢,这个方法确实很快。
      • 正是我想要的。 :)
      【解决方案5】:

      问题自然是 O(n),因此您的解决方案可能是最有效的。

      由于您尝试计算位的任意子集,因此您无法在设置位时对其进行计数(如果您不经常设置位,将提供速度提升)。

      您可以检查您正在使用的处理器是否有返回设置位数的命令。例如,具有 SSE4 的处理器可以使用 POPCNT according to this post。这可能对您不起作用,因为.Net 不允许组装(因为它独立于平台)。此外,ARM 处理器可能没有同类产品。

      可能最好的解决方案是查找表(或者,如果您可以保证开关将编译为单个跳转到 currentLocation + byteValue,则可以使用开关)。这将为您提供整个字节的计数。当然,BitArray 不提供对底层数据类型的访问权限,因此您必须制作自己的 BitArray。您还必须保证字节中的所有位始终是交集的一部分,这听起来不太可能。

      另一种选择是使用布尔数组而不是 BitArray。这具有不需要从字节中的其他位中提取位的优点。缺点是数组将占用 8 倍的内存空间,这意味着不仅会浪费空间,还会在您遍历数组以执行计数时推送更多数据。

      标准数组查找和BitArray查找的区别如下:
      数组:

      1. 偏移量 = 索引 * 索引大小
      2. 在位置 + 偏移处获取内存并保存到值

      位数组:

      1. 索引 = 索引/索引大小
      2. 偏移量 = 索引 * 索引大小
      3. 在位置 + 偏移处获取内存并保存到值
      4. 位置 = index%indexSize
      5. 移位值位置位
      6. 值 = 值和 1

      除了#2 for Arrays 和#3 之外,这些命令中的大多数都需要 1 个处理器周期才能完成。一些命令可以使用 x86/x64 处理器组合成 1 个命令,但可能不适用于 ARM,因为它使用减少的指令集。
      两者中哪一个(阵列或位阵列)性能更好取决于您的平台(处理器速度、处理器指令、处理器缓存大小、处理器缓存速度、系统内存量 (Ram)、系统内存速度 (CAS)、处理器和 RAM 之间的连接)以及要计算的索引的分布(是最常聚集的交叉点还是随机分布的)。

      总结一下:您可能会找到一种方法来加快速度,但您的解决方案是使用 .NET 中的每布尔模型位为您的数据集所获得的最快解决方案。

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

      【讨论】:

        【解决方案6】:
        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 字节。

        【讨论】:

          【解决方案7】:

          如果你不介意将 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);
                  }
              }
          

          【讨论】:

            【解决方案8】:

            如果您真的想最大化速度,您可以预先计算一个查找表,其中给定一个字节值您有基数,但 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 &amp;= tmp - 1,为了清除最低有效设置位,已翻译为tmp &amp;= (tmp &amp; ~0x1).

            对于 BCL BitArray 的情况,我不知道这是否比天真的迭代更快,但从算法上讲它应该更好。


            编辑:引用了我发现 Kernighan 技巧的地方,并提供了更深入的解释

            【讨论】:

            • 您的代码tmp = tmp.And (tmp.And (NOT_ONE)); 似乎不起作用。在 tmp 和 NOT_ONE 之间执行 And 将导致 tmp 的最低有效位设置为 0,所有其他位将保持不变。在 tmp 和 tmp0(其中 tmp0 的最低位设置为 0)之间执行和将导致 tmp0,因为 1 and 1 是 1 and 0 并且任何东西都是 0。这将导致第一次迭代将最低有效位设置为 0,但所有其他迭代都不会做任何事情(除非我想念一些东西)。
            【解决方案9】:

            使用BitArray 没有更快的方法 - 归根结底是您必须计算它们 - 您可以使用 LINQ 来执行此操作或执行您自己的循环,但 BitArray 没有提供任何方法并且底层数据结构是一个int[] 数组(如 Reflector 所示) - 所以这总是 O(n),n 是数组中的位数。

            我能想到让它更快的唯一方法是使用反射来获取底层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,但跟踪设置的位数并提供适当的方法(主要委托给@987654328 @ 但是添加方法来获取当前设置的位数) - 当然这将是 O(1)。

            【讨论】:

            • 如果优化对你来说真的很重要,我会说你应该自己拿一个 int 并玩弄它,而不是完全弄乱 BitArray ;)
            • 如果我想计算创建类实例后设置的位,我自己的包装类将按照您的建议工作。但我将它用于交集,然后计算结果中的位数(bresult = b1.And(b2))。但是你的反射概念给了我一个想法。我看得更深,发现该类有一个私有属性 _version 似乎有计数。我能想到的唯一方法是使用反射。所以让我检查一下这是否比我的直接循环更快。
            • @Sam:我认为_version 只是在此BitArray 实例上执行的更改次数。
            【解决方案10】:

            你可以使用 Linq,但它没用而且速度更慢:

            var sum = mybitarray.OfType<bool>().Count(p => p);
            

            【讨论】:

            • 这只是我写的东西的漫长方式。它们翻译成完全相同的东西。运行时是相同的,那么您反对 Linq 的论点在哪里?
            • 您指望一切都将得到优化……您不能指望它。在旧版本的 .net 中,foreach 和 for(对于数组)的速度不同。我还没有对 IEnumerable 接口和 [] 访问器之间更快的速度进行基准测试,但是“通常”linq 更慢(因为某些方法并不总是内联,而 OP 代码将总是“内联”,因为它已经内联了) .你是对的,它不是无用的,它只是“不是真的有用”。这似乎是 linq 的练习(就像优雅的练习)。
            • 是的,我可以使用 linq(这两种方法中的任何一种),但两者都比我的 For 循环(在位数组的情况下)慢,并且无论如何都是 O(n) 操作。
            【解决方案11】:

            您可以使用 Linq 轻松完成此操作

            BitArray ba = new BitArray(new[] { true, false, true, false, false });
            var numOnes = (from bool m in ba
                       where m
                       select m).Count();
            

            【讨论】:

            • 如果使用 LINQ,则为上述的单行变体:ba.Cast&lt;bool&gt;().Count(l =&gt; l)。最后,这只是一个变相的foreach循环。
            猜你喜欢
            • 2016-10-31
            • 2011-09-29
            • 1970-01-01
            • 2021-07-18
            • 2016-02-21
            • 1970-01-01
            • 2016-11-27
            相关资源
            最近更新 更多