30

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

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

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

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

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

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

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

11 回答 11

24

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

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

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

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

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

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

于 2009-09-10T17:21:19.950 回答
12
  • 以任何顺序轻松设置多个标志。

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

于 2009-09-10T17:16:44.807 回答
8

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

于 2009-09-10T17:17:28.797 回答
6

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

于 2009-09-10T17:23:10.547 回答
3

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

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

于 2009-09-10T17:19:22.637 回答
3

从领域模型的角度来看,它只是在某些情况下更好地模拟现实。如果您有三个布尔值,例如 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 变量中是很有用的……它表示一个域元素,其值可以表示任何可能的状态组合。

于 2009-09-10T17:44:34.450 回答
2

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

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

于 2009-09-10T17:23:56.270 回答
2

Raymond Chen 有一篇关于这个主题的博文

当然,位域可以节省数据内存,但您必须在代码大小、可调试性和减少多线程方面的成本之间取得平衡。

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

于 2009-09-10T17:24:26.083 回答
1
  1. 空间效率 - 1 位
  2. 时间效率 - 位比较由硬件快速处理。
  3. 语言独立性 - 数据可以由许多不同的程序处理,您无需担心跨不同语言/平台的布尔值的实现。

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

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

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

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

于 2009-09-10T17:52:11.933 回答
0

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

if ((flags & AnchorStyles.Top) == AnchorStyles.Top)
{
    //Do stuff
} 
于 2009-09-10T17:16:04.870 回答
0

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

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

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

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

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

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

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

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

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

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

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

于 2021-12-05T17:03:44.017 回答