【问题标题】:Is BitArray faster in C# for getting a bit value than a simple conjuction with bitwise shift?C# 中的 BitArray 是否比使用按位移位的简单结合更快地获取位值?
【发布时间】:2013-05-04 12:26:19
【问题描述】:

1)。 var bitValue = (byteValue & (1 << bitNumber)) != 0;

2)。使用 System.Collections.BitArrayGet(int index) 方法

  • 什么更快?
  • 在 .NET 项目的哪些情况下,BitArray 可能比与按位移位的简单结合更有用?

【问题讨论】:

  • 使用System.Diagnostics.Stopwatch,如果它更快,你可以计时。最好在尽可能靠近生产环境的地方试用。

标签: c# performance bit-shift logical-operators bitarray


【解决方案1】:

BitArray 将能够处理任意数量的布尔值,而byte 将仅容纳 8,int 仅容纳 32,等等。这将是两者之间的最大区别。

另外,BitArray 实现了IEnumerable,而整型显然没有。所以这一切都取决于你的项目的要求;如果您需要IEnumerable 或类似数组的接口,请使用BitArray

我实际上会在任一解决方案上使用bool[],因为它在您要跟踪的什么类型的数据中更加明确。 T

BitArraybitfield 将使用大约 1/8 的空间 bool[] 因为它们将 8 个布尔值“打包”到一个字节中,而 bool 本身将占用整个 8-位字节。使用位域或 BitArray 的空间优势并不重要,除非您存储 lotsbools。 (数学由读者决定:-))


基准测试

结果:对于我的原始测试环境,BitArray 似乎快了 bit,但与使用整数类型自己做的数量级相同。还测试了bool[],不出所料,这是最快的。访问内存中的单个字节将比访问不同字节中的单个位复杂。

Testing with 10000000 operations:
   A UInt32 bitfield took 808 ms.
   A BitArray (32) took 574 ms.
   A List<bool>(32) took 436 ms.

代码:

class Program
{
    static void Main(string[] args)
    {
        Random r = new Random();
        r.Next(1000);

        const int N = 10000000;

        Console.WriteLine("Testing with {0} operations:", N);

        Console.WriteLine("   A UInt32 bitfield took {0} ms.", TestBitField(r, N));
        Console.WriteLine("   A BitArray (32) took {0} ms.", TestBitArray(r, N));
        Console.WriteLine("   A List<bool>(32) took {0} ms.", TestBoolArray(r, N));

        Console.Read();
    }


    static long TestBitField(Random r, int n)
    {
        UInt32 bitfield = 0;

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < n; i++) {

            SetBit(ref bitfield, r.Next(32), true);
            bool b = GetBit(bitfield, r.Next(32));
            SetBit(ref bitfield, r.Next(32), b);
        }
        sw.Stop();
        return sw.ElapsedMilliseconds;
    }

    static bool GetBit(UInt32 x, int bitnum) {
        if (bitnum < 0 || bitnum > 31)
            throw new ArgumentOutOfRangeException("Invalid bit number");

        return (x & (1 << bitnum)) != 0;
    }

    static void SetBit(ref UInt32 x, int bitnum, bool val)
    {
        if (bitnum < 0 || bitnum > 31)
            throw new ArgumentOutOfRangeException("Invalid bit number");

        if (val)
            x |= (UInt32)(1 << bitnum);
        else
            x &= ~(UInt32)(1 << bitnum);
    }



    static long TestBitArray(Random r, int n)
    {
        BitArray b = new BitArray(32, false);     // 40 bytes

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < n; i++) {

            b.Set(r.Next(32), true);
            bool v = b.Get(r.Next(32));
            b.Set(r.Next(32), v);
        }
        sw.Stop();
        return sw.ElapsedMilliseconds;
    }



    static long TestBoolArray(Random r, int n)
    {
        bool[] ba = new bool[32];

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < n; i++) {

            ba[r.Next(32)] = true;
            bool v = ba[r.Next(32)];
            ba[r.Next(32)] = v;
        }
        sw.Stop();
        return sw.ElapsedMilliseconds;
    }
}

【讨论】:

  • 我已经从原帖中删除了第二个问题,然后重新打开。有趣的是,我在当前项目中有一堆 SetBit 和 GetBit 函数,看起来就像这些。
  • 此外,您的代码似乎测试了随机数生成器的速度以及位移。如果随机数生成需要更长的时间,我不会感到惊讶。
  • @RobertHarvey 你是对的,但我并不太担心。 A) 随机数的生成应该是相当恒定的,并且在所有方法之间都是一样的,所以可以忽略它。 B)要做到这一点而不定时生成随机数会更复杂,也不能很好地测试功能。如果您有不同的想法,我会很高兴听到它。
  • 我在我的机器上运行你的代码并没有改变,结果分别为 1525ms 和 1185ms。然后我将您的random r 更改为int,将其设置为零,然后再次运行。结果分别为 581 毫秒和 209 毫秒,这表明 BitArray 的速度是原来的两倍多,而 Random 则消耗了 2/3 的处理时间。我将r 设置为 31 得到了基本相同的结果(两次运行我使用了 0 和 31)。
  • @Trap 有更好的想法吗?
【解决方案2】:

@乔纳森·莱因哈特,

很遗憾,您的基准没有定论。它没有考虑可能的延迟加载、缓存和/或预取(由 CPU、主机操作系统和/或 .NET 运行时)的影响。

打乱测试的顺序(或多次调用测试方法),您可能会注意到不同的时间测量。

我使用“任何 CPU”平台目标和 .NET 4.0 客户端配置文件构建了您的原始基准测试,并在我的具有 i7-3770 CPU 和 64 位 Windows 7 的机器上运行。

我得到的是这样的:

Testing with 10000000 operations:
   A UInt32 bitfield took 484 ms.
   A BitArray (32) took 459 ms.
   A List<bool>(32) took 393 ms.

这与您的观察非常吻合。

但是,在 UInt32 测试之前执行 BitArray 测试会产生以下结果:

Testing with 10000000 operations:
   A BitArray (32) took 513 ms.
   A UInt32 bitfield took 456 ms.
   A List<bool>(32) took 417 ms.

通过查看 UInt32 和 BitArray 测试的时间,您会注意到测量的时间似乎与测试本身无关,而是与测试运行的顺序有关。

为了至少一点点减轻这些副作用,我在每个程序运行中执行了两次测试方法,结果如下。

测试顺序UInt32、BitArray、BoolArray、UInt32、BitArray、BoolArray

Testing with 10000000 operations:
   A UInt32 bitfield took 476 ms.
   A BitArray (32) took 448 ms.
   A List<bool>(32) took 367 ms.

   A UInt32 bitfield took 419 ms.  <<-- Watch this.
   A BitArray (32) took 444 ms.    <<-- Watch this.
   A List<bool>(32) took 388 ms.

测试顺序BitArray、UInt32、BoolArray、BitArray、UInt32、BoolArray

Testing with 10000000 operations:
   A BitArray (32) took 514 ms.
   A UInt32 bitfield took 413 ms.
   A List<bool>(32) took 379 ms.

   A BitArray (32) took 444 ms.    <<-- Watch this.
   A UInt32 bitfield took 413 ms.  <<-- Watch this.
   A List<bool>(32) took 381 ms.

查看测试方法的第二次调用,似乎至少在具有最新 .NET 运行时的 i7 CPU 上,UInt32 测试比 BitArray 测试快,而BoolArray 测试仍然是最快的。

(很抱歉,我不得不写下我对 Jonathon 基准的回复作为答案,但作为一个新的 SO 用户,我不能发表评论......)

编辑:

您可以尝试在调用第一个测试之前放置一个 Thread.Sleep(5000) 或类似的方法,而不是打乱测试方法的顺序...

原来的测试似乎也让 UInt32 测试处于劣势,因为它包含一个边界检查“if (bitnum 31)”,它被执行了 3000 万次。其他两个测试都不包括这样的边界检查。然而,这实际上并不是全部真相,因为 BitArray 和 bool 数组都在内部进行边界检查。

虽然我没有测试,但我希望消除边界检查将使 UInt32 和 BoolArray 测试的性能相似,但这对于公共 API 来说不是一个好的提议。

【讨论】:

  • 您真的应该完全独立地运行所有测试,而不是一个接一个地运行。
  • @Seph,你是对的。对于适当的基准,这将是要走的路。然而,我写的代码只是为了展示著名的原则“你不衡量你认为你正在衡量的东西”;)
【解决方案3】:

将 BitArray 用于以 List 表示时适合缓存的数据没有性能意义。

所展示的基准指出了显而易见的事实: 由于缺乏计算要求,布尔列表将比位数组执行得更快。

但是,这些测试的最大问题是它们在 32 大小的数组上运行。这意味着整个数组都适合缓存。当您开始进行大量内存提取时,处理大量布尔值的成本就会显现出来。

与包含 5000 项的列表相比,包含 5000 项的 BitArray 的性能将大不相同。 List 需要比 BitArray 多 8 倍的内存读取。

根据您的其余逻辑(您正在执行多少分支和其他操作),差异可能很小或很大。内存预取允许 CPU 将下一个预测的内存块拉入缓存,同时仍在处理第一个块。如果您正在对数据结构进行干净的直接迭代,您可能看不到显着的性能差异。另一方面,如果您正在执行一些使 CPU 难以预测内存提取的分支或操作,则更有可能看到性能差异。

此外,如果您开始谈论 MULTIPLE 数据结构,事情就会变得更加模糊。如果您的代码依赖于对 100 个不同位数组的引用怎么办?好的,现在我们正在讨论更多的数据,即使 BitArrays 本身很小,而且您将四处跳来访问不同的 BitArrays,因此访问模式会影响事情。

如果不对您的特定算法/场景进行基准测试,就不可能知道真实行为。

【讨论】:

    【解决方案4】:

    如果有人仍在寻找一些足够快的不同解决方案:我建议在 GetBit 和 SetBit 方法上使用积极的内联 [MethodImpl(256)]。还具有位位置的 OR 和 XOR 值的查找表。删除位位置检查,因为 System.IndexOutOfRangeException 足以指示位位置错误,我们不需要消耗 CPU 进行额外检查。 此外,如果对大量元素执行此操作并进行调试,强烈建议使用 [DebuggerHidden] 属性。 DebuggerHidden 属性帮助调试器跳过使用该属性标记的方法的调试并加快调试过程。

    使用Jonathon Reinhart answer 中的代码并为 TestBitFieldOpt 和 TestBitFieldOpt2 添加此方法和测试。

        static readonly uint[] BITMASK = new uint[]
        {
            0x00000001, 0x00000002, 0x00000004, 0x00000008, 0x00000010, 0x00000020, 0x00000040, 0x00000080,
            0x00000100, 0x00000200, 0x00000400, 0x00000800, 0x00001000, 0x00002000, 0x00004000, 0x00008000,
            0x00010000, 0x00020000, 0x00040000, 0x00080000, 0x00100000, 0x00200000, 0x00400000, 0x00800000,
            0x01000000, 0x02000000, 0x04000000, 0x08000000, 0x10000000, 0x20000000, 0x40000000, 0x80000000
        };
    
        static readonly uint[] BITMASK_XOR = new uint[]
        {
            0xFFFFFFFE, 0xFFFFFFFD, 0xFFFFFFFB, 0xFFFFFFF7, 0xFFFFFFEF, 0xFFFFFFDF, 0xFFFFFFBF, 0xFFFFFF7F,
            0xFFFFFEFF, 0xFFFFFDFF, 0xFFFFFBFF, 0xFFFFF7FF, 0xFFFFEFFF, 0xFFFFDFFF, 0xFFFFBFFF, 0xFFFF7FFF,
            0xFFFEFFFF, 0xFFFDFFFF, 0xFFFBFFFF, 0xFFF7FFFF, 0xFFEFFFFF, 0xFFDFFFFF, 0xFFBFFFFF, 0xFF7FFFFF,
            0xFEFFFFFF, 0xFDFFFFFF, 0xFBFFFFFF, 0xF7FFFFFF, 0xEFFFFFFF, 0xDFFFFFFF, 0xBFFFFFFF, 0x7FFFFFFF
        };
    
        static long TestBitFieldOpt(Random r, int n)
        {
            bool value;
            UInt32 bitfield = 0;
            var sw = Stopwatch.StartNew();
            for (int i = 0; i < n; i++)
            {
                SetBitOpt(ref bitfield, r.Next(32), true);
                value = GetBitOpt(bitfield, r.Next(32));
                SetBitOpt(ref bitfield, r.Next(32), value);
            }
            sw.Stop();
            return sw.ElapsedMilliseconds;
        }
    
        static long TestBitFieldOpt2(Random r, int n)
        {
            bool value;
            UInt32 bitfield = 0;
            var sw = Stopwatch.StartNew();
            for (int i = 0; i < n; i++)
            {
                bitfield = SetBitOpt2(bitfield, r.Next(32), true);
                value = GetBitOpt(bitfield, r.Next(32));
                bitfield = SetBitOpt2(bitfield, r.Next(32), value);
            }
            sw.Stop();
            return sw.ElapsedMilliseconds;
        }
    
        [MethodImpl(256)]
        static bool GetBitOpt(UInt32 bitfield, int bitindex)
        {
            return (bitfield & BITMASK[bitindex]) != 0;
        }
    
        [MethodImpl(256)]
        static void SetBitOpt(ref UInt32 bitfield, int bitindex, bool value)
        {
            if (value)
                bitfield |= BITMASK[bitindex];
            else
                bitfield &= BITMASK_XOR[bitindex];
        }
    
        [MethodImpl(256)]
        static UInt32 SetBitOpt2(UInt32 bitfield, int bitindex, bool value)
        {
            if (value)
                return (bitfield | BITMASK[bitindex]);
            return (bitfield & BITMASK_XOR[bitindex]);
        }
    

    我将测试次数增加了 10 次方(以获得更真实的结果),优化代码的结果非常接近最快的方法:

        Testing with 100000000 operations:
        A BitArray (32) took      : 4947 ms.
        A UInt32 bitfield took    : 4399 ms.
        A UInt32 bitfieldopt      : 3583 ms.
        A UInt32 bitfieldopt2     : 3583 ms.
        A List<bool>(32) took     : 3491 ms.
    

    通常,本地堆栈上的变量越少,分支越少,并且预先计算的值在大多数情况下都会获胜。所以拿起书和铅笔并缩短数学以减少那些......真正的内联函数有很大帮助,但更好地在方法上使用 [MethodImpl(256)] 属性以提高可读性/保持源代码,如上所示.

    希望这可以帮助某人找到解决问题的方法。

    【讨论】:

      猜你喜欢
      • 2016-02-21
      • 2021-10-18
      • 1970-01-01
      • 1970-01-01
      • 2016-11-27
      • 1970-01-01
      • 2016-10-31
      相关资源
      最近更新 更多