【问题标题】:c# bitarray index of positive bitsc# 正位的bitarray索引
【发布时间】:2011-11-16 21:00:41
【问题描述】:

我有一个长度相当大 (500,000) 的 c# BitArray,我正在尝试获取数组中设置的所有正位的索引。目前我正在通过以下方式实现这一目标:

public int[] GetIndexesForPositives()
{
    var idIndexes = new int[GetPositiveCount + 1];
    var idx = 0;
    for (var i = 0; i < Length; i++)
        {
            if (Get(i))
            {
                idIndexes[idx++] = i;
            }
        }
    return idIndexes;
}

我创建一个已知正位大小的空数组,然后我在该位数组上进行 lopp 并将索引值添加到返回数组中。

这意味着我必须在数组上执行 500,000 次循环,而且速度并不快。 (大约需要 15 毫秒)。

我知道 BitArray 在幕后使用了一个整数数组(我用它来编写 GetPositiveCount 函数 - 通过一个我从堆栈中下来的算法),我想知道是否也有一个算法可以做到这一点?

【问题讨论】:

  • 并且该算法不需要具有 500K 次迭代的循环? (我的观点:即使这还不够快,您也可以尝试更改任何除了循环)。
  • 也许递归是错误的词,我只是想避免循环 500k 次。 id 更倾向于遍历 BitArray 类中的底层整数数组,这将涉及更少的循环。
  • 如果这不是过早的优化,您可以使用循环展开并将循环减少到 125.000。但是您从这种优化中获得了什么?我认为没什么。
  • @AaronHS 也许您的意思是您正在迭代整个集合。
  • @PVitt 感谢展开的建议。我做到了,它有所作为。除非你愿意,否则我会把它作为答案发布?关于过早的优化,我认为在不了解更多背景的情况下质疑动机是不公平的:)

标签: c# performance algorithm


【解决方案1】:

@Seph 在https://stackoverflow.com/a/7415891/264022 中的回答非常好,但还可以做得更快。

  1. 先统计位数
  2. 展开内部循环(一次检查 4 位) 2b。做一个构造,让 JIT 做一个跳转表而不是一堆 IF(这成为唯一不可预测的分支)
  3. 使用安全的 C# 指针魔法来避免越界检查

结果比@Seph 的快速版本快大约 3-4 倍。

  public static int[] GetIndexesForPositives(ulong[] _array)
  {
     var thisArray = _array;

     var c2 = 0ul;
     for (var i = 0; i < thisArray.Length; i++)
     {
        var v = thisArray[i];
        //from http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
        v = v - ((v >> 1) & (ulong)~(ulong)0/3);                           // temp
        v = (v & (ulong)~(ulong)0/15*3) + ((v >> 2) & (ulong)~(ulong)0/15*3);      // temp
        v = (v + (v >> 4)) & (ulong)~(ulong)0/255*15;                      // temp
        var c = (ulong) (v * ((ulong) ~(ulong) 0 / 255)) >> (sizeof(ulong) - 1) * 8; // count
        c2 += c;
     }
     if (c2 == 0) return Array.Empty<int>();

     var idIndexes = new int[c2];
     ref var idxref = ref idIndexes[0];
     for (var i = 0; i < thisArray.Length; i++)
     {
        var v = thisArray[i];

        for (var j = 0; j < 64; j += 4)
        {
           if (v == 0)
              break;
           var b = ((byte)v) & 0b1111;
           var actualIndex = i * 64 + j;
           switch (b)
           {
           case 0: break;
           case 0b001:
              idxref = (actualIndex);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b010:
              idxref = (actualIndex+1);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b011:
              idxref = (actualIndex);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+1);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b100:
              idxref = (actualIndex + 2);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b101:
              idxref = (actualIndex);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+2);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b110:
              idxref = (actualIndex+1);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+2);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b111:
              idxref = (actualIndex);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+1);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+2);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b1000: 
              idxref = (actualIndex+3);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b1001:
              idxref = (actualIndex);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+3);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b1010:
              idxref = (actualIndex+1);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+3);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b1011:
              idxref = (actualIndex);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+1);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+3);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b1100:
              idxref = (actualIndex + 2);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+3);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b1101:
              idxref = (actualIndex);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+2);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+3);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b1110:
              idxref = (actualIndex+1);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+2);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+3);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;
           case 0b1111:
              idxref = (actualIndex);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+1);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+2);
              idxref = ref Unsafe.Add(ref idxref, 1);
              idxref = (actualIndex+3);
              idxref = ref Unsafe.Add(ref idxref, 1);
              break;

           }

           v >>= 4;
        }
     }
     return idIndexes;
  }

【讨论】:

    【解决方案2】:

    如果您能够在 BitArray 下获得一个 int 数组,这应该会提供更好的性能:

    假设您不知道设置的位数:

    public static int[] GetIndexesForPositives()
    {
        var idIndexes = new List<int>();
        System.Reflection.FieldInfo field = data.GetType().GetField("m_array", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        int[] values = field.GetValue(data) as int[];
    
        for (var i = 0; i < values.Length; i++)
        {
            int _i = values[i];
            if (_i != 0)
            {
                for (var j = 0; j < 32; j++)
                {
                    if ((_i & (1 << j)) != 0)
                    {
                        idIndexes.Add(i * 32 + j);
                    }
                }
            }
        }
        return idIndexes.ToArray();
    }
    

    如果您确实知道设置的位数,您可以这样做:

    public static int[] GetIndexesForPositives(int length)
    {
        var idIndexes = new int[length];
        var idx = 0;
        System.Reflection.FieldInfo field = data.GetType().GetField("m_array", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        int[] values = field.GetValue(data) as int[];
    
        for (var i = 0; i < values.Length; i++)
        {
            int _i = values[i];
            if (_i != 0)
            {
                for (var j = 0; j < 32; j++)
                {
                    if ((_i & (1 << j)) != 0)
                    {
                        idIndexes[idx++] = i * 32 + j;
                    }
                }
            }
    }
    

    我的测试使这两个方法比您的方法运行得更快,即使是一开始不知道返回数组有多大的方法。

    我使用包含 5000 万条记录的随机 BitArray 测试的结果:

    1) 25001063 records found in 50000000, took 1415.5752ms
    2) 25001063 records found in 50000000, took 1099.67ms
    3) 25001063 records found in 50000000, took 1045.6862ms
    4) 25001063 records found in 50000000, took 745.7762ms"
    
    1) is your code but using an arraylist instead of using some `GetPositiveCount` to get the output length.
    2) is your code
    3) is my (revised) first example
    4) is my (revised) second example
    

    编辑:此外,值得指出的是,这是一个真正受益于多线程的问题。将 ByteArray 分成 4 个部分,您就有 4 个线程可以同时运行检查数据。

    编辑:我知道这已经被接受,但是如果您知道大多数情况下您的列表会非常稀疏,那么您可以采取其他措施来提高性能:

    for (var j = 0; j < 32; j++)
    {
         if (_i == 0)
             break;
         if ((_i & (1)) != 0)
         {
             idIndexes.Add(i * 32 + j);
         }
         _i = _i >> 1;
     }
    

    当列表填充 >40% 或更多时,它会稍微慢一些,但是如果您知道列表总是 10% 1 和 90% 0,那么这对您来说会运行得更快。

    【讨论】:

    • 为了这个工作,我必须先将位数组转换为字节数组,但不是吗?
    • BCL 中的 BitArray 在内部使用整数数组,而不是字节。它被称为“m_array”,@AaronHS 你可以通过反射得到它
    • 是的,我知道。这就是我实现 GetPositiveCount 方法的方式。反映复制和扩展,因为它被密封:(
    • 仅供参考,将底层的 int[] 转换为 byte[] 然后执行上面的,速度只有一半。
    • 我已经更新了我的答案以使用 m_array 而不是字节数组,性能虽然稍好一些,但与我开始使用的字节数组基本相同。
    【解决方案3】:

    你相当坚持迭代 500,000 次。直观地说:如果设置了每一位,那么您将需要生成一个包含 500,000 个元素的数组。您可以通过访问底层 byte 或 int 将外部迭代次数减少 8 或 32 倍,但您仍然必须迭代这些位。即使是每个可能的字节值都有 256 个元素的查找表也无济于事,因为您必须添加位索引。

    但是,如果您有很多零位(当然希望您这样做),那么如果底层字节/int 为 0,则优化是简单地将循环计数器调整 8 或 32。既然您知道 Get() 将返回0. 另外,使用 List 来避免 GetPostitiveCount 的开销。

    完全消除对这个数组的需求并延迟检索设置为 1 的下一位将是迄今为止最好的方法。

    【讨论】:

    • 谢谢,是的,分块和位移产生了巨大的影响
    【解决方案4】:

    如果您可以将 BCL 中的 BitArray 换成“自己动手”,那么您可以做得更好。以下是您可以做的一些事情:

    1. 跳过没有设置位的 64 块
    2. 对于确实有位的 64 块,仅使用 x &amp; (x - 1) 枚举 1 位而不是所有位,您最喜欢的快速 2log 找到 here(使用简单的 64 步方法不会给出任何类型的加速)
    3. 保留一个额外的位数组,用于存储每个 64 位块是否为非零。将项目符号 2 中的技术应用于 那个 位数组,以一次性跳过整个零范围。
    4. 将项目符号 3 递归应用于巨大的位数组

    所有这四个仅在预期位数组是稀疏的情况下才有帮助,如果它不是稀疏的,最坏的情况仍然是 O(n)。如果应用子弹 3 直到顶部为单个 ulong,则它可以在 O(1) 中确定整个位数组是否为空。

    【讨论】:

      【解决方案5】:

      我会这样做:

      public int[] GetIndexesForPositives()
      {
          var idIndexes = new LinkedList<int>();
      
          for (var i = 0; i < Length; i++)
              {
                  if (Get(i))
                  {
                      idIndexes.Add(i);
                  }
              }
          return idIndexes.ToArray();
      }
      

      如果这仍然不可接受(因为您在执行 ToArray 时再次遍历 indizes)只需为结果数组使用相同大小并返回找到的 indizes 的长度:

      public int GetIndexesForPositives(out int[] indizes)
      {
          indizes = new int[Length];
          var idI = 0;
      
          for (var i = 0; i < Length; i++)
              {
                  if (Get(i))
                  {
                      indizes[idI++] = i;
                  }
              }
          return idI;
      }
      

      根据你是否真的需要所有的 indizes 或只需要部分,你甚至可以考虑这样的事情(但如果你需要每个部分,它的性能会降低 - 请自己做一些分析):

      public IEnumerable<int> GetIndexesForPositives()
      {
          for (var i = 0; i < Length; i++)
              {
                  if (Get(i))
                  {
                      yield return i;
                  }
              }
      }
      

      这是假设您的 Get(i) 正在完成它的工作并且您的数组是不可变的。

      【讨论】:

      • 不幸的是第一个有点慢,第二个是一样的。懒惰评估的人并不会真正有所作为,因为我总是在做整个数组
      • 对不起。我想那是因为您已经知道数组的计数 - 也许您可以更改它以一次性创建数组和计数 - 这样我的第二个解决方案应该会更快,因为它可以两者兼得。
      • 顺便说一句:如果你可以使用集合/列表,你可以尝试在没有 ToArray 的情况下进行第一个 - 但我想你不会比你拥有的更快跨度>
      猜你喜欢
      • 2016-11-27
      • 2016-02-21
      • 2016-10-31
      • 2017-05-31
      • 2012-05-30
      • 2010-09-08
      • 2020-04-04
      • 2016-01-08
      相关资源
      最近更新 更多