为什么使用标志位掩码而不是一系列布尔值?
考虑到我有一个对象可能处于一个或多个真/假状态,我一直有点模糊为什么程序员经常使用标志+位掩码而不是只使用几个布尔值。
一切都在 .NET 框架上。不确定这是否是最好的示例,但 .NET 框架具有以下功能:
public enum AnchorStyles
{
None = 0,
Top = 1,
Bottom = 2,
Left = 4,
Right = 8
}
因此,给定锚点样式,我们可以使用位掩码来确定选择了哪些状态。但是,似乎您可以使用 AnchorStyle 类/结构来完成同样的事情,其中为每个可能的值或单个枚举值的数组定义了 bool 属性。
当然,我的问题的主要原因是我想知道我是否应该对我自己的代码遵循类似的做法。
那么,为什么要使用这种方法呢?
- 内存消耗更少? (它似乎不会比布尔数组/结构消耗更少)
- 堆栈/堆性能比结构或数组更好?
- 比较操作更快?更快的增值/减值?
- 对于编写它的开发人员来说更方便吗?
Given a case where I have an object that may be in one or more true/false states, I've always been a little fuzzy on why programmers frequently use flags+bitmasks instead of just using several boolean values.
It's all over the .NET framework. Not sure if this is the best example, but the .NET framework has the following:
public enum AnchorStyles
{
None = 0,
Top = 1,
Bottom = 2,
Left = 4,
Right = 8
}
So given an anchor style, we can use bitmasks to figure out which of the states are selected. However, it seems like you could accomplish the same thing with an AnchorStyle class/struct with bool properties defined for each possible value, or an array of individual enum values.
Of course the main reason for my question is that I'm wondering if I should follow a similar practice with my own code.
So, why use this approach?
- Less memory consumption? (it doesn't seem like it would consume less than an array/struct of bools)
- Better stack/heap performance than a struct or array?
- Faster compare operations? Faster value addition/removal?
- More convenient for the developer who wrote it?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
传统上,这是减少内存使用的一种方法。所以,是的,它在 C# 中已经过时了 :-)
作为一种编程技术,它在当今的系统中可能已经过时了,并且您可以使用布尔数组,但是......
比较存储的值很快作为位掩码。使用 AND 和 OR 逻辑运算符并比较生成的 2 个整数。
它使用的内存要少得多。将所有 4 个示例值放入位掩码中将使用半个字节。使用布尔数组时,很可能会使用几个字节作为数组对象,并为每个布尔使用一个长字。如果您必须存储一百万个值,您就会明白为什么位掩码版本更优越。
它更容易管理,您只需要处理单个整数值,而布尔数组在数据库中的存储方式会完全不同。
而且,由于内存布局,在各个方面都比数组快得多。它几乎与使用单个 32 位整数一样快。我们都知道这是数据操作所能达到的最快速度。
It was traditionally a way of reducing memory usage. So, yes, its quite obsolete in C# :-)
As a programming technique, it may be obsolete in today's systems, and you'd be quite alright to use an array of bools, but...
It is fast to compare values stored as a bitmask. Use the AND and OR logic operators and compare the resulting 2 ints.
It uses considerably less memory. Putting all 4 of your example values in a bitmask would use half a byte. Using an array of bools, most likely would use a few bytes for the array object plus a long word for each bool. If you have to store a million values, you'll see exactly why a bitmask version is superior.
It is easier to manage, you only have to deal with a single integer value, whereas an array of bools would store quite differently in, say a database.
And, because of the memory layout, much faster in every aspect than an array. It's nearly as fast as using a single 32-bit integer. We all know that is as fast as you can get for operations on data.
以任何顺序轻松设置多个标志。
轻松保存并获取一系列 0101011 到数据库。
Easy setting multiple flags in any order.
Easy to save and get a serie of 0101011 to the database.
除此之外,向位字段添加新的位含义比向类添加新的布尔值更容易。将位域从一个实例复制到另一个实例也比一系列布尔值更容易。
Among other things, its easier to add new bit meanings to a bitfield than to add new boolean values to a class. Its also easier to copy a bitfield from one instance to another than a series of booleans.
它还可以使方法更加清晰。想象一个具有 10 个布尔值与 1 个位掩码的方法。
It can also make Methods clearer. Imagine a Method with 10 bools vs. 1 Bitmask.
从领域模型的角度来看,它只是在某些情况下更好地模拟现实。如果您有像 AccountIsInDefault 和 IsPreferredCustomer 以及 RequiresSalesTaxState 这样的三个布尔值,那么将它们添加到单个 Flags 装饰枚举中是没有意义的,因为它们不是同一域模型元素的三个不同值。
但是,如果您有一组布尔值,例如:
或
那么能够将帐户(或货物)的总状态存储在一个变量中...代表一个域元素,其值可以代表任何可能的组合的国家。
From a domain Model perspective, it just models reality better in some situations. If you have three booleans like AccountIsInDefault and IsPreferredCustomer and RequiresSalesTaxState, then it doesnn't make sense to add them to a single Flags decorated enumeration, cause they are not three distinct values for the same domain model element.
But if you have a set of booleans like:
or
Then it is useful to be able to store the total state of the Account, (or the cargo) in ONE variable... that represents ONE Domain Element whose value can represent any possible combination of states.
Raymond Chen 有关于此主题的博客文章。
正如其他人所说,它的时代基本上已经过去了。仍然很想这样做,因为位摆弄很有趣而且看起来很酷,但它不再更有效率,它在维护方面有严重的缺点,它不能很好地与数据库配合,除非你在一个嵌入式世界,你有足够的内存。
Raymond Chen has a blog post on this subject.
As others have said, its time is largely past. It's tempting to still do it, cause bit fiddling is fun and cool-looking, but it's no longer more efficient, it has serious drawbacks in terms of maintenance, it doesn't play nicely with databases, and unless you're working in an embedded world, you have enough memory.
我建议永远不要使用枚举标志,除非您正在处理一些非常严重的内存限制(不太可能)。您应该始终编写针对维护进行优化的代码。
拥有多个布尔属性可以更轻松地阅读和理解代码、更改值并提供 Intellisense 注释,更不用说减少出现错误的可能性。如有必要,您始终可以在内部使用枚举标志字段,只需确保使用布尔属性公开值的设置/获取即可。
I would suggest never using enum flags unless you are dealing with some pretty serious memory limitations (not likely). You should always write code optimized for maintenance.
Having several boolean properties makes it easier to read and understand the code, change the values, and provide Intellisense comments not to mention reduce the likelihood of bugs. If necessary, you can always use an enum flag field internally, just make sure you expose the setting/getting of the values with boolean properties.
实际上,它可以具有更好的性能,主要是如果您的枚举源自字节。
在这种极端情况下,每个枚举值将由一个字节表示,包含所有组合,最多 256 个。拥有如此多可能的布尔值组合将导致 256 个字节。
但即便如此,我也不认为这就是真正的原因。我更喜欢这些的原因是 C# 赋予我处理这些枚举的能力。我可以使用一个表达式添加多个值。我也可以删除它们。我什至可以使用枚举同时将多个值与单个表达式进行比较。使用布尔值,代码可以变得更加冗长。
Actually, it can have a better performance, mainly if your enum derives from an byte.
In that extreme case, each enum value would be represented by a byte, containing all the combinations, up to 256. Having so many possible combinations with booleans would lead to 256 bytes.
But, even then, I don't think that is the real reason. The reason I prefer those is the power C# gives me to handle those enums. I can add several values with a single expression. I can remove them also. I can even compare several values at once with a single expression using the enum. With booleans, code can become, let's say, more verbose.
大多数时候,这些在维护方面不值得权衡。然而,有时它是有用的:
修改标题的成本:数百万美元和多年的努力。
将信息硬塞到标头中未使用的 2 个字节中的成本:0。
当然,访问和操作此信息的代码中存在额外成本,但无论如何这些都是由函数完成的,因此一旦您有了访问器定义它的可维护性并不比使用布尔值差。
Most of the time, these are not worth the tradeoff in terms of maintance. However, there are times when it is useful:
Cost to modify the header: millions of dollars and years of effort.
Cost to shoehorn the information into 2 bytes in the header that weren't being used: 0.
Of course, there was the additional cost in the code that accessed and manipulated this information, but these were done by functions anyways so once you had the accessors defined it was no less maintainable than using Booleans.
这是为了速度和效率。本质上,您所使用的只是一个整数。
It is for speed and efficiency. Essentially all you are working with is a single int.
我已经看到了诸如时间效率和兼容性之类的答案。这些是原因,但我认为没有解释为什么这些在我们这样的时代有时是必要的。从与其他工程师聊天的所有答案和经验中,我看到它被描绘成某种奇怪的旧时代做事方式,这种方式应该消失,因为新的做事方式更好。
是的,在极少数情况下,出于性能考虑,您可能希望以“旧方式”执行此操作,就像您有经典的百万次循环一样。但我说这是错误的看待事物的角度。
虽然确实,您根本不应该关心,并使用任何 C# 语言作为新的正确方式™ 来做事(由一些花哨的 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 通道(将其分配给外设)并使用它。位域将满足所有这些要求,并且仅使用十几条指令来执行分配,这些指令可内联在请求代码中。基本上你不能比这更快,你的代码只是几个函数,基本上我们将困难的部分委托给硬件来解决问题,约束:仅限位域
假设一个外围设备执行其职责需要普通 RAM 内存中的一些工作空间。例如,假设一个使用分散-聚集 DMA 的高速 I/O 外设,简而言之,它使用一个固定大小的 RAM 块,其中填充了下一次传输的描述(顺便说一句,描述符本身由位域组成),并将一个链接到彼此在 RAM 中创建一个 FIFO 传输队列。应用程序首先准备描述符,然后与当前传输的尾部链接,而无需暂停控制器(甚至不禁用中断)。此类描述符的分配/释放可以使用位域和事务指令进行,因此当它在不同的 CPU 之间以及驱动程序中断和内核之间共享时,所有描述符仍然可以正常工作而不会发生冲突。一种使用情况是内核以原子方式分配描述符,而不停止或禁用中断,并且没有额外的锁(位域本身就是锁),中断在传输完成时取消分配。
大多数旧策略是预分配资源并强制应用程序在使用后释放。
如果您需要在 steriods 上使用多任务,C# 允许您使用线程 + 互锁,但最近 C# 引入了轻量级任务,猜猜它是如何制作的?使用 Interlocked 类进行事务性内存访问。因此,您可能不需要重新发明轮子,任何低级别部分都已被覆盖并经过精心设计。
所以我们的想法是,让聪明的人(不是我,我是像你一样的普通开发人员)为你解决困难的部分,并享受像 C# 这样的通用计算平台。如果您仍然看到这些部分的一些残余,是因为有人可能仍然需要与 .NET 之外的世界进行交互并访问一些驱动程序或系统调用,例如要求您知道如何构建描述符并将每个位放在正确的位置。不要生那些人的气,他们让我们的工作成为可能。
简而言之:互锁+位域。非常强大,请勿使用它
I have seen answers like Time efficiency and compatibility. those are The Reasons, but I do not think it is explained why these are sometime necessary in times like ours. from all answers and experience of chatting with other engineers I have seen it pictured as some sort of quirky old time way of doing things that should just die because new way to do things are better.
Yes, in very rare case you may want to do it the "old way" for performance sake like if you have the classic million times loop. but I say that is the wrong perspective of putting things.
While it is true that you should NOT care at all and use whatever C# language throws at you as the new right-way™ to do things (enforced by some fancy AI code analysis slaping you whenever you do not meet their code style), you should understand deeply that low level strategies aren't there randomly and even more, it is in many cases the only way to solve things when you have no help from a fancy framework. your OS, drivers, and even more the .NET itself(especially the garbage collector) are built using bitfields and transactional instructions. your CPU instruction set itself is a very complex bitfield, so JIT compilers will encode their output using complex bit processing and few hardcoded bitfields so that the CPU can execute them correctly.
When we talk about performance things have a much larger impact than people imagine, today more then ever especially when you start considering multicores.
when multicore systems started to become more common all CPU manufacturer started to mitigate the issues of SMP with the addition of dedicated transactional memory access instructions while these were made specifically to mitigate the near impossible task to make multiple CPUs to cooperate at kernel level without a huge drop in perfomrance it actually provides additional benefits like an OS independent way to boost low level part of most programs. basically your program can use CPU assisted instructions to perform memory changes to integers sized memory locations, that is, a read-modify-write where the "modify" part can be anything you want but most common patterns are a combination of set/clear/increment.
usually the CPU simply monitors if there is any other CPU accessing the same address location and if a contention happens it usually stops the operation to be committed to memory and signals the event to the application within the same instruction. this seems trivial task but superscaler CPU (each core has multiple ALUs allowing instruction parallelism), multi-level cache (some private to each core, some shared on a cluster of CPU) and Non-Uniform-Memory-Access systems (check threadripper CPU) makes things difficult to keep coherent, luckily the smartest people in the world work to boost performance and keep all these things happening correctly. todays CPU have a large amount of transistor dedicated to this task so that caches and our read-modify-write transactions work correctly.
C# allows you to use the most common transactional memory access patterns using Interlocked class (it is only a limited set for example a very useful clear mask and increment is missing, but you can always use CompareExchange instead which gets very close to the same performance).
To achieve the same result using a array of booleans you must use some sort of lock and in case of contention the lock is several orders of magnitude less permorming compared to the atomic instructions.
here are some examples of highly appreciated HW assisted transaction access using bitfields which would require a completely different strategy without them of course these are not part of C# scope:
assume a DMA peripheral that has a set of DMA channels, let say 20 (but any number up to the maximum number of bits of the interlock integer will do). When any peripheral's interrupt that might execute at any time, including your beloved OS and from any core of your 32-core latest gen wants a DMA channel you want to allocate a DMA channel (assign it to the peripheral) and use it. a bitfield will cover all those requirements and will use just a dozen of instructions to perform the allocation, which are inlineable within the requesting code. basically you cannot go faster then this and your code is just few functions, basically we delegate the hard part to the HW to solve the problem, constraints: bitfield only
assume a peripheral that to perform its duty requires some working space in normal RAM memory. for example assume a high speed I/O peripheral that uses scatter-gather DMA, in short it uses a fixed-size block of RAM populated with the description (btw the descriptor is itself made of bitfields) of the next transfer and chained one to each other creating a FIFO queue of transfers in RAM. the application prepares the descriptors first and then it chains with the tail of the current transfers without ever pausing the controller (not even disabling the interrupts). the allocation/deallocation of such descriptors can be made using bitfield and transactional instructions so when it is shared between diffent CPUs and between the driver interrupt and the kernel all will still work without conflicts. one usage case would be the kernel allocates atomically descriptors without stopping or disabling interrupts and without additional locks (the bitfield itself is the lock), the interrupt deallocates when the transfer completes.
most old strategies were to preallocate the resources and force the application to free after usage.
If you ever need to use multitask on steriods C# allows you to use either Threads + Interlocked, but lately C# introduced lightweight Tasks, guess how it is made? transactional memory access using Interlocked class. So you likely do not need to reinvent the wheel any of the low level part is already covered and well engineered.
so the idea is, let smart people (not me, I am a common developer like you) solve the hard part for you and just enjoy general purpose computing platform like C#. if you still see some remnants of these parts is because someone may still need to interface with worlds outside .NET and access some driver or system calls for example requiring you to know how to build a descriptor and put each bit in the right place. do not being mad at those people, they made our jobs possible.
In short : Interlocked + bitfields. incredibly powerful, don't use it