我已经看到了诸如时间效率和兼容性之类的答案。这些是原因,但我认为这无法解释为什么在我们这样的时代有时需要这些。从与其他工程师聊天的所有答案和经验中,我看到它被描绘成某种古怪的旧时做事方式,因为新的做事方式更好,所以应该死掉。是的,在极少数情况下,出于性能考虑,您可能希望以“旧方式”执行此操作,就像您拥有经典的百万次循环一样。但我说这是错误的看待事物的观点。
虽然您完全不应该关心并使用任何 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 之外的世界交互并访问一些驱动程序或系统调用,例如要求您知道如何构建描述符并将每个位放在正确的位置。不要生那些人的气,他们让我们的工作成为可能。
简而言之:互锁+位域。非常强大,不要使用它