【问题标题】:Why use flags+bitmasks rather than a series of booleans?为什么使用标志+位掩码而不是一系列布尔值?
【发布时间】:2009-09-10 17:12:13
【问题描述】:

假设我有一个对象可能处于一个或多个真/假状态,我一直对为什么程序员经常使用标志+位掩码而不是只使用几个布尔值有点模糊。

它遍布 .NET 框架。不确定这是否是最好的示例,但 .NET 框架具有以下内容:

public enum AnchorStyles
{
    None = 0,
    Top = 1,
    Bottom = 2,
    Left = 4,
    Right = 8
}

因此,给定锚样式,我们可以使用位掩码来确定选择了哪些状态。但是,您似乎可以使用 AnchorStyle 类/结构来完成相同的事情,该类/结构为每个可能的值定义了 bool 属性,或者一个单独的枚举值数组。

当然,我提出问题的主要原因是我想知道我是否应该在自己的代码中遵循类似的做法。

那么,为什么要使用这种方法?

  • 内存消耗少? (它似乎不会消耗少于布尔数组/结构的消耗)
  • 比结构或数组更好的堆栈/堆性能?
  • 更快的比较操作?更快的增值/去除价值?
  • 对编写它的开发者来说更​​方便?

【问题讨论】:

  • 并不是我认为这是一个强有力的论点,但它确实消耗更少的内存。它使用了一个 int(4 个字节),而每个 bool 使用了一个字节。因此,4 个布尔值与 1 个 int 值相同。 32 个布尔值占用 32 个字节,而所有这些布尔值都可以在同一个枚举中。如果你去不推荐的路径,你可以使枚举长度为 8 个字节(sizeof(long))。
  • 感谢您澄清这一点。它把我带到了这个帖子:stackoverflow.com/questions/294905/…
  • 因此从响应中可以清楚地看出,枚举标志在内存方面比布尔结构/数组更轻量级。但是,似乎也有一些非常适合该任务的 .NET 框架类,例如 BitVector32 或 BitArray。使用 BitVector32(由 uint 支持)进行存储并提供在特定索引处获取/设置位(作为布尔值)的属性的结构呢? Windows 窗体似乎可以做到这一点。为开发人员提供更多代码,但似乎它会表现良好,并且封装将使下游 API 使用者更容易使用。嗯?

标签: c# enums flags


【解决方案1】:

传统上这是一种减少内存使用的方法。所以,是的,它在 C# 中已经过时了:-)

作为一种编程技术,它在今天的系统中可能已经过时了,你可以使用 bool 数组,但是......

比较存储为位掩码的值很快。使用 AND 和 OR 逻辑运算符并比较生成的 2 个整数。

它使用的内存要少得多。将所有 4 个示例值放在位掩码中将使用半个字节。使用 bool 数组,很可能会为数组对象使用几个字节加上每个 bool 的长字。如果你必须存储一百万个值,你就会明白为什么位掩码版本更好。

它更容易管理,您只需要处理一个整数值,而布尔数组的存储方式则完全不同,比如数据库。

而且,由于内存布局,在各个方面都比数组快得多。它几乎与使用单个 32 位整数一样快。我们都知道对数据进行操作的速度是最快的。

【讨论】:

    【解决方案2】:
    • 以任意顺序轻松设置多个标志。

    • 易于保存和获取一系列 0101011 到数据库。

    【讨论】:

    【解决方案3】:

    除其他外,向位域添加新的位含义比向类添加新的布尔值更容易。将位域从一个实例复制到另一个实例也比一系列布尔值更容易。

    【讨论】:

    • 在我看来,将布尔值添加到类就像:bool newState;关于复制,复制一个结构似乎同样容易。
    • @Winston:序列化格式发生变化,并且接受旧数据默认值的良好序列化程序,以及旧版本不会丢弃未知字段的良好序列化程序很难找到。二进制接口发生变化,这可能会导致一系列所需的更新,并且需要对结构进行完整的验证支持。 (当然 contract 必须明确声明“未知位被忽略”或“未知位导致错误”)。此外,在实现层面上,将它们作为一个整体来处理也更容易。
    • @Winston 如果您创建了 API 会怎样?然后每个可能升级到您的新版本的人都必须更改那里的代码,因为一个新的布尔值被添加到一个方法中。虽然如果它是一个枚举,那么必须在此处进行任何更改以保持使用它的相同代码。这就是 .NET 框架偏爱枚举而不是布尔值的原因。
    • @David - 我不是在谈论向方法添加参数,而是向结构添加 bool 字段,这不会影响任何调用方法,但对于@Peter 而言,会影响序列化。
    • 向 C# 结构/类添加新字段并不是一个重大更改(除了以前的代码不知道它)。在这两种情况下,都必须更新现有代码以了解如何使用新标志。
    【解决方案4】:

    它还可以使方法更清晰。想象一个有 10 个布尔值和 1 个位掩码的方法。

    【讨论】:

      【解决方案5】:

      实际上,它可以有更好的性能,主要是如果你的枚举来自一个字节。 在这种极端情况下,每个枚举值将由一个字节表示,包含所有组合,最多 256 个。有这么多可能的布尔组合将导致 256 个字节。

      但是,即便如此,我也不认为这是真正的原因。我更喜欢这些的原因是 C# 赋予我处理这些枚举的能力。我可以用一个表达式添加多个值。我也可以删除它们。我什至可以使用枚举一次将多个值与单个表达式进行比较。比方说,使用布尔值,代码可以变得更冗长。

      【讨论】:

      • 共有 256 种组合,但只有 8 个标志。不要混淆他们。
      • 256 种使用 bool 的组合?它是 8 个布尔值。 8 个布尔值不是 256 字节。
      【解决方案6】:

      从领域模型的角度来看,它只是在某些情况下更好地模拟现实。如果您有三个布尔值,例如 AccountIsInDefault 和 IsPreferredCustomer 和 RequiresSalesTaxState,那么将它们添加到单个 Flags 修饰的枚举中没有意义,因为它们不是同一个域模型元素的三个不同值。

      但是,如果您有一组布尔值,例如:

       [Flags] enum AccountStatus {AccountIsInDefault=1, 
               AccountOverdue=2 and AccountFrozen=4}
      

        [Flags] enum CargoState {ExceedsWeightLimit=1,  
               ContainsDangerousCargo=2, IsFlammableCargo=4, 
               ContainsRadioactive=8}
      

      然后,能够将帐户的总状态(或货物)存储在 ONE 变量中是很有用的……它表示一个域元素,其值可以表示任何可能的状态组合。

      【讨论】:

        【解决方案7】:

        Raymond Chen 有a blog post on this subject

        当然,位域可以节省数据内存,但是 你必须平衡它与 代码大小、可调试性和成本 减少多线程。

        正如其他人所说,它的时代已经过去了。仍然很想这样做,因为小玩意很有趣而且看起来很酷,但它不再更有效率,它在维护方面有严重的缺陷,它不能很好地与数据库一起工作,除非你在一个嵌入式世界,你有足够的内存。

        【讨论】:

        • Raymond 说的是位域,而不是位掩码。
        【解决方案8】:

        我建议不要使用枚举标志,除非你正在处理一些非常严重的内存限制(不太可能)。您应该始终编写为维护而优化的代码。

        拥有多个布尔属性可以更轻松地阅读和理解代码、更改值并提供 Intellisense cmets,更不用说减少错误的可能性。如有必要,您始终可以在内部使用枚举标志字段,只需确保使用布尔属性公开设置/获取值。

        【讨论】:

          【解决方案9】:
          1. 空间效率 - 1 位
          2. 时间效率 - 位比较由硬件快速处理。
          3. 语言独立 - 数据可能由多个不同的程序处理,您无需担心布尔值跨不同语言/平台的实现。

          大多数时候,这些在维护方面不值得权衡。但是,有时它很有用:

          1. 网络协议 - 减少消息大小将大大节省
          2. 旧版软件 - 曾经我必须添加一些信息以跟踪某些旧版软件。

          修改标题的成本:数百万美元和多年的努力。 将信息硬塞到标头中未使用的 2 个字节的成本:0。

          当然,访问和操作这些信息的代码会产生额外的成本,但无论如何这些都是由函数完成的,所以一旦定义了访问器,它的可维护性不亚于使用布尔值。

          【讨论】:

          • 1.空间效率仅适用于非常密集的包装或极其有限的环境; 2. 时间效率取决于对掩码的有效使用(与比较单个布尔值相比,掩码和比较单个位肯定快); 3. 不适用,错误使用布尔类型就是错误使用布尔类型。
          【解决方案10】:

          这是为了速度和效率。基本上,您所使用的只是一个 int。

          if ((flags & AnchorStyles.Top) == AnchorStyles.Top)
          {
              //Do stuff
          } 
          

          【讨论】:

          • 这是一个相当高级的答案。您能否具体说明哪些操作更快/更高效,为什么?或链接到证明您的主张合理的文章?
          • 我真的需要证明使用本机类型和简单的逻辑表达式是快速高效的吗?
          • 不要忘记操作的顺序。您必须在此处的按位运算周围加上括号。
          • 很好,我已经习惯了 Visual Studio 的支持。
          • -1 没有为“速度和效率”提供任何理由,在这种情况下,我怀疑两者都不是。考虑反式/建议:if (AnchorsTop) { .. }
          【解决方案11】:

          我已经看到了诸如时间效率和兼容性之类的答案。这些就是原因,但我认为这无法解释为什么在我们这样的时代有时需要这些。从与其他工程师聊天的所有答案和经验中,我看到它被描绘成某种古怪的旧时做事方式,因为新的做事方式更好,所以应该死掉。 是的,在极少数情况下,出于性能考虑,您可能希望以“旧方式”进行操作,就像您拥有经典的百万次循环一样。但我说这是错误的看待事物的角度。

          虽然确实你根本不应该关心,并使用 C# 语言作为新的 right-way™ 做事(由一些花哨的 AI 代码分析强制执行,无论何时你不符合他们的代码风格),你应该深刻理解低级策略不是随机存在的,甚至更多,在很多情况下,当你没有花哨的框架帮助时,这是解决问题的唯一方法。您的操作系统、驱动程序,甚至更多 .NET 本身(尤其是垃圾收集器)都是使用位域和事务指令构建的。您的 CPU 指令集本身是一个非常复杂的位域,因此 JIT 编译器将使用复杂的位处理和少量硬编码位域对其输出进行编码,以便 CPU 可以正确执行它们。

          当我们谈论性能时,事情的影响比人们想象的要大得多,尤其是当您开始考虑多核时。

          当多核系统开始变得更加普遍时,所有 CPU 制造商都开始通过添加专用的事务性内存访问指令来缓解 SMP 问题,而这些专门用于缓解几乎不可能完成的任务,即让多个 CPU 在内核级别进行协作在性能没有大幅下降的情况下,它实际上提供了额外的好处,例如独立于操作系统的方式来提升大多数程序的低级部分。基本上,您的程序可以使用 CPU 辅助指令来执行对整数大小的内存位置的内存更改,即读取-修改-写入,其中“修改”部分可以是您想要的任何内容,但最常见的模式是设置/清除/的组合增量。 通常,CPU 只是监视是否有任何其他 CPU 访问同一地址位置,如果发生争用,它通常会停止提交到内存的操作,并在同一条指令中向应用程序发送事件信号。这似乎是微不足道的任务,但超大规模 CPU(每个内核都有多个允许指令并行的 ALU)、多级缓存(一些为每个内核私有,一些在 CPU 集群上共享)和非统一内存访问系统(检查 threadripper CPU ) 使事情难以保持连贯性,幸运的是,世界上最聪明的人致力于提高性能并保持所有这些事情正确发生。今天的 CPU 有大量专门用于此任务的晶体管,以便缓存和我们的读-修改-写事务正常工作。 C# 允许您使用 Interlocked 类使用最常见的事务性内存访问模式(它只是一个有限的集合,例如一个非常有用的清除掩码并且缺少增量,但您始终可以使用 CompareExchange 代替它非常接近相同的性能) .

          要使用布尔数组获得相同的结果,您必须使用某种锁,并且在发生争用时,与原子指令相比,锁的执行率要低几个数量级。

          这里有一些高度赞赏的使用位域的硬件辅助事务访问的示例,如果没有它们,这将需要完全不同的策略,当然这些不属于 C# 范围:

          • 假设一个具有一组 DMA 通道的 DMA 外设,比如说 20 个(但任何数字都可以达到互锁整数的最大位数)。当任何外围设备的中断可能随时执行时,包括您心爱的操作系统和您最新一代 32 核的任何内核需要一个 DMA 通道,您想要分配一个 DMA 通道(将其分配给外围设备)并使用它。一个位域将涵盖所有这些要求,并将仅使用十几条指令来执行分配,这些指令可在请求代码中内联。基本上你不能比这更快,你的代码只是几个函数,基本上我们将困难的部分委托给硬件来解决问题,约束:bitfield only

          • 假设执行其职责的外围设备需要正常 RAM 内存中的一些工作空间。例如,假设一个高速 I/O 外设使用 scatter-gather DMA,简而言之,它使用一个固定大小的 RAM 块,其中填充了下一次传输的描述(顺便说一句,描述符本身由位域组成)并将一个链接到彼此在 RAM 中创建一个 FIFO 传输队列。应用程序首先准备描述符,然后与当前传输的尾部链接,而无需暂停控制器(甚至不禁用中断)。此类描述符的分配/解除分配可以使用位域和事务指令进行,因此当它在不同的 CPU 之间以及驱动程序中断和内核之间共享时,所有这些仍然可以正常工作而不会发生冲突。一种用例是内核在不停止或禁用中断且没有附加锁(位域本身就是锁)的情况下自动分配描述符,当传输完成时中断解除分配。 大多数旧策略是预先分配资源并在使用后强制应用程序释放。

          如果您需要在 steriods 上使用多任务 C# 允许您使用线程 + 互锁,但最近 C# 引入了轻量级任务,猜猜它是如何制作的?使用 Interlocked 类的事务性内存访问。因此,您可能不需要重新发明轮子,任何低级部分已经被覆盖和精心设计。

          所以想法是,让聪明的人(不是我,我是像你这样的普通开发人员)为你解决困难的部分,并享受像 C# 这样的通用计算平台。如果您仍然看到这些部分的一些残余,是因为有人可能仍然需要与 .NET 之外的世界交互并访问一些驱动程序或系统调用,例如要求您知道如何构建描述符并将每个位放在正确的位置。不要生那些人的气,他们让我们的工作成为可能。

          简而言之:互锁 + 位域。非常强大,不要使用它

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-08-08
            • 2013-12-17
            • 1970-01-01
            • 2020-08-03
            • 2015-02-19
            • 1970-01-01
            • 2022-11-08
            • 2011-01-29
            相关资源
            最近更新 更多