我有一个问题要问所有的铁杆低级黑客。我在博客中看到了这句话。我真的不认为来源很重要(如果你真的在乎,那就是 Haack),因为它似乎是一个常见的陈述。
例如,许多现代 3-D 游戏都有用 C++ 和汇编编写的高性能核心引擎。
就程序集而言 - 是用程序集编写的代码,因为您不希望编译器发出额外的指令或使用过多的字节,或者您正在使用无法用 C 表达的更好的算法(或无法表达没有编译器把它们弄乱了)?
我完全明白理解底层的东西很重要。我只是想在你理解它之后理解为什么程序在汇编中。
我有一个问题要问所有的铁杆低级黑客。我在博客中看到了这句话。我真的不认为来源很重要(如果你真的在乎,那就是 Haack),因为它似乎是一个常见的陈述。
例如,许多现代 3-D 游戏都有用 C++ 和汇编编写的高性能核心引擎。
就程序集而言 - 是用程序集编写的代码,因为您不希望编译器发出额外的指令或使用过多的字节,或者您正在使用无法用 C 表达的更好的算法(或无法表达没有编译器把它们弄乱了)?
我完全明白理解底层的东西很重要。我只是想在你理解它之后理解为什么程序在汇编中。
我认为您误读了以下声明:
例如,许多现代 3-D 游戏都有用 C++ 和汇编编写的高性能核心引擎。
游戏(以及如今的大多数程序)与“用 C++ 编写”不同的是“用汇编语言编写”。那个博客并不是说游戏的很大一部分是用汇编设计的,或者一群程序员坐在那里以汇编作为他们的主要语言进行开发。
这真正意味着开发人员首先编写游戏并使其在 C++ 中运行。然后他们对其进行分析,找出瓶颈是什么,如果值得,他们会在组装中优化它们。或者,如果他们已经有经验,他们知道哪些部分会成为瓶颈,并且他们已经从他们构建的其他游戏中获得了优化的部分。
汇编中的编程要点与往常一样:速度。在汇编程序中编写大量代码会很荒谬,但是编译器没有意识到一些优化,并且对于足够小的代码窗口,人类会做得更好。
例如,对于浮点,编译器往往非常保守,并且可能不知道您的体系结构的一些更高级的特性。如果您愿意接受一些错误,通常可以比编译器做得更好,如果您发现花费大量时间在汇编中编写一小段代码是值得的。
以下是一些更相关的示例:
游戏中的例子
来自英特尔的关于使用 SSE 内在函数优化游戏引擎的文章。最终代码使用了intrinsic(不是inline assembler),所以纯汇编的量很小。但他们会查看编译器的汇编器输出,以准确确定要优化的内容。
Quake 的快速反平方根。同样,该例程中没有汇编程序,但您需要了解一些有关架构的知识才能进行这种优化。作者知道哪些操作快(乘法、移位),哪些操作慢(除法、sqrt)。所以他们提出了一个非常棘手的平方根实现,完全避免了缓慢的操作。
高性能计算
在游戏领域之外,科学计算领域的人们经常对事物进行优化,以使它们能够在最新的硬件上快速运行。把它想象成你不能在物理上作弊的游戏。
最近的一个很好的例子是Lattice Quantum Chromodynamics (Lattice QCD)。 本文描述了该问题如何归结为一个非常小的计算内核,该内核针对IBM Blue Gene/L上的 PowerPC 440 进行了大量优化。每个 440 都有两个 FPU,它们支持一些特殊的三元运算,编译器很难利用这些运算。如果没有这些优化,Lattice QCD 的运行速度会慢得多,当您的问题需要在昂贵的机器上运行数百万小时的 CPU 时,这将是代价高昂的。
如果您想知道为什么这很重要,请查看来自这项工作的 Science 中的文章。使用 Lattice QCD,这些人根据第一原理计算了质子的质量,并在去年表明 90% 的质量来自强结合能,其余来自夸克。这就是E=mc 2的作用。 这是一个摘要。
对于上述所有情况,应用程序并不是100% 以汇编方式设计或编写的——甚至没有接近。但是当人们真正需要速度时,他们会专注于编写代码的关键部分以在特定硬件上运行。
我已经很多年没有用汇编语言编码了,但我可以给出几个我经常看到的原因:
并非所有编译器都可以利用某些 CPU 优化和指令集(例如,英特尔偶尔添加的新指令集)。等待编译器编写者赶上来意味着失去竞争优势。
更容易将实际代码与已知的 CPU 架构和优化相匹配。例如,您了解的获取机制、缓存等。这应该对开发人员是透明的,但事实并非如此,这就是编译器编写者可以优化的原因。
某些硬件级别的访问只能通过汇编语言进行(例如,在编写设备驱动程序时)。
汇编语言的形式推理有时实际上比高级语言更容易,因为您已经知道代码的最终或几乎最终布局是什么。
在没有 API 的情况下编程某些 3D 图形卡(大约 1990 年代后期)在汇编语言中通常更实用和高效,有时在其他语言中则不可能。但同样,这涉及到基于加速器架构的真正专家级游戏,例如以特定顺序手动将数据移入和移出。
我怀疑很多人在使用高级语言时会使用汇编语言,尤其是当该语言是 C 时。手动优化大量通用代码是不切实际的。
汇编程序编程的一个方面是其他人没有提到的——当您知道应用程序中的每个字节都是您自己努力的结果,而不是编译器的结果时,您会感到满足。我一秒钟都不想像 80 年代初那样用汇编程序编写整个应用程序,但有时我确实怀念那种感觉……
在我的第一份工作(80 年代)中,我开始使用汇编语言进行专业编程。对于嵌入式系统,内存需求——RAM 和 EPROM——很低。您可以编写易于占用资源的紧凑代码。
到 80 年代后期,我已经切换到 C。代码更容易编写、调试和维护。非常小的代码片段是用汇编程序编写的——对我来说,这是我在自己滚动的 RTOS 中编写上下文切换的时候。(除非它是一个“科学项目”,否则你不应该再做的事情。)
您将在一些 Linux 内核代码中看到汇编程序片段。最近我在自旋锁和其他同步代码中浏览过它。这些代码需要访问原子测试和设置操作、操作缓存等。
我认为您很难为大多数通用编程优化现代 C 编译器。
我同意@altCognito 的观点,你的时间可能最好花在更努力地思考问题并做得更好。出于某种原因,程序员经常关注微观效率而忽视宏观效率。汇编语言提高性能是一种微观效率。退后一步以更广泛地了解系统可以暴露系统中的宏观问题。解决宏观问题通常可以带来更好的性能提升。一旦解决了宏观问题,就会崩溃到微观层面。
我想微观问题在单个程序员的控制范围内,并且在较小的域中。在宏观层面上改变行为需要与更多的人交流——一些程序员会避免这样做。整个牛仔vs团队的事情。
“是的”。但是,要明白,在大多数情况下,用汇编程序编写代码的好处并不值得付出努力。在汇编中编写它所获得的回报往往比仅仅专注于更努力地思考问题并花时间思考更好的做事方式所获得的回报要小。
主要负责编写 Quake 的 John Carmack 和 Michael Abrash 以及进入 IDs 游戏引擎的所有高性能代码在本书中详细介绍了这一点。
我也同意 Ólafur Waage 的观点,即今天,编译器非常聪明,并且经常采用许多利用隐藏架构提升的技术。
如今,至少对于顺序代码,一个体面的编译器几乎总能胜过经验丰富的汇编语言程序员。但对于矢量代码,这是另一回事。例如,广泛部署的编译器在利用 x86 SSE 单元的矢量并行功能方面做得并不好。我是一名编译器编写者,利用 SSE是我自己而不是信任编译器的理由。
SSE 代码在汇编中比编译器内在函数更好,至少在 MSVC 中。(即不创建额外的数据副本)
我的源代码中有三到四个汇编程序例程(大约 20 MB 源代码)。所有这些都是SSE(2),并且与(相当大 - 认为 2400x2048 和更大)图像的操作有关。
出于爱好,我在编译器上工作,那里有更多的汇编器。运行时库通常充满了它们,它们中的大多数都与违反正常程序制度的东西有关(比如异常的助手等)
我的微控制器没有任何汇编程序。大多数现代微控制器都有如此多的外围硬件(中断控制的计数器,甚至整个正交编码器和串行构建块),以至于通常不再需要使用汇编程序来优化循环。按照目前的闪存价格,代码存储器也是如此。此外,通常还有一系列引脚兼容的设备,因此如果您系统地用完 cpu 电源或闪存空间,升级通常不是问题
除非你真的出货了 100000 台设备,并且编程汇编器可以通过在闪存芯片中安装更小的类别来真正节省大量资金。但我不属于这一类。
很多人认为嵌入式是汇编程序的借口,但他们的控制器比开发Unix的机器拥有更多的 CPU 能力。(Microchip 配备 40 和 60 MIPS微控制器,价格低于10美元)。
然而,很多人都被遗留问题所困扰,因为改变微芯片架构并不容易。此外,HLL 代码非常依赖于体系结构(因为它使用硬件外围设备、寄存器来控制 I/O 等)。因此,有时有充分的理由继续在汇编程序中维护项目(我很幸运能够从头开始在新架构上设置事务)。但人们常常自欺欺人说他们真的需要汇编程序。
当我们问我们是否可以使用 GOTO 时,我仍然喜欢一位教授给出的答案(但您也可以将其读为 ASSEMBLER):“如果您认为值得写一篇 3 页的文章说明您为什么需要该功能,您可以使用它. 请连同你的结果一起提交论文。”
我将其用作低级功能的指导原则。不要太拥挤而无法使用它,但要确保你正确地激励它。甚至设置一两个人为的障碍(如文章)以避免将复杂的推理作为正当理由。
缺陷倾向于逐行运行(语句、代码点等);虽然对于大多数问题,汇编确实会比高级语言使用更多的行,但有时它是最好的(最简洁,最少的行)映射到手头的问题。这些案例中的大多数都涉及通常的嫌疑人,例如嵌入式系统中的驱动程序和位碰撞。
一些指令/标志/控制根本不存在于 C 级别。
例如,检查 x86 上的溢出是简单的溢出标志。此选项在 C 中不可用。
游戏非常需要性能,尽管同时优化器非常好,但“大师级程序员”仍然能够通过在组装中手动编码正确的部分来挤出更多的性能。
永远不要在没有先分析它的情况下开始优化你的程序。分析后应该能够识别瓶颈,如果找到更好的算法等不再削减它,您可以尝试在汇编中手动编写一些东西。
如果您参与了所有 Y2K 修复工作,那么如果您了解 Assembly,您可能会赚很多钱。仍然有很多遗留代码是写在里面的,而且这些代码有时需要维护。
如果您正在编程具有 128 字节 RAM 和 4K 程序存储器的低端 8 位微控制器,那么您在使用汇编时没有太多选择。有时,尽管在使用功能更强大的微控制器时,您需要在准确的时间执行某些操作。汇编语言很有用,因为您可以计算指令并测量代码使用的时钟周期。
另一个原因可能是可用的编译器对于架构来说不够好,并且程序中所需的代码量没有那么长或复杂,以至于程序员会迷失在其中。尝试为嵌入式系统编写微控制器,通常组装会容易得多。
除了提到的其他事情之外,所有高级语言都有一定的局限性。这就是为什么有些人选择在 ASM 中编程,以完全控制他们的代码。
其他人则喜欢非常小的可执行文件,在 20-60KB 的范围内,例如检查HiEditor,它是由 HiEdit 控件的作者实现的,用于 Windows 的极好的强大的编辑控件,语法高亮和标签只有 ~50kb)。在我的收藏中,我有超过 20 个这样的黄金控件,从 Excell 像 ssheets 到 html 渲染。
我想很多游戏开发者会对这些信息感到惊讶。
我所知道的大多数游戏都尽可能少地使用组装。在某些情况下,根本没有,最坏的情况是一两个循环或函数。
这句话过于笼统,远没有十年前那么真实。
但是,嘿,单纯的事实不应该阻碍真正的黑客为支持组装而进行的十字军东征。;)
它似乎没有被提及,所以我想我会添加它:在现代游戏开发中,我认为至少有一些正在编写的程序集根本不适合 CPU。它以着色器程序的形式用于 GPU 。
这可能出于各种原因需要,有时仅仅是因为使用的任何高级着色语言都不允许以所需指令的确切数量来表达确切的操作,以适应某些大小约束、速度或任何组合. 就像汇编语言编程一样,我猜。
除了非常小的 CPU 上的非常小的项目之外,我不会着手对整个项目进行汇编编程。但是,通常会发现性能瓶颈可以通过一些内部循环的策略性手工编码来缓解。
在某些情况下,真正需要的只是用优化器无法弄清楚如何使用的指令替换某些语言结构。一个典型的例子是在 DSP 应用程序中,向量运算和乘法累加运算对于优化器来说很难发现,但很容易编写代码。
例如,某些型号的 SH4 包含 4x4 矩阵和 4 个向量指令。通过将 3x3 矩阵上的等效 C 操作替换为适当的指令,我看到了颜色校正算法的巨大性能改进,代价是将校正矩阵扩大到 4x4 以匹配硬件假设。这是通过编写不超过 12 行汇编来实现的,并对相关数据类型和存储进行匹配调整到周围 C 代码中的少数几个地方。
迄今为止,我见过的几乎所有中型到大型游戏引擎或库都有一些手动优化的汇编版本,可用于矩阵运算,例如 4x4 矩阵连接。在处理大型矩阵时,编译器似乎不可避免地会错过一些巧妙的优化(重用寄存器、以最有效的方式展开循环、利用特定于机器的指令等)。这些矩阵操作函数也几乎总是配置文件上的“热点”。
我还看到手动编码的程序集大量用于自定义调度——比如 FastDelegate,但编译器和机器特定。
最后,如果你有中断服务例程,asm 可以改变世界——有些操作你只是不想在中断下发生,你希望你的中断处理程序“快速进入和退出”。 .. 如果它在 asm 中,您几乎完全知道 ISR 中会发生什么,并且它鼓励您保持简短(无论如何这是一个好习惯)。
我继续做的唯一汇编程序编码是用于资源匮乏的嵌入式硬件。正如利德所提到的,汇编仍然非常适合需要快速且易于理解的代码的ISR 。
对我来说,第二个原因是保持我对汇编功能的了解。能够检查和理解 CPU 执行我的指令所采取的步骤感觉很好。
我只亲自与一位开发人员谈过他对汇编的使用。他正在研究处理便携式 mp3 播放器控制的固件。组装工作有两个目的:
很多人喜欢诋毁汇编语言,因为他们从来没有学过用它编码,只是模糊地遇到过它,这让他们感到震惊或有些害怕。真正有才华的程序员会明白,抨击 C 或汇编是毫无意义的,因为它们是互补的。事实上,一个的优点就是另一个的缺点。C 的有组织的语法规则提高了清晰度,但同时放弃了所有强大的集合,因为没有任何结构规则!C 代码指令用于创建非阻塞代码,这可能会强制明确编程意图,但这是一种功耗。在 C 中,编译器不允许在 if/elseif/else/end 内跳转。或者您不允许在彼此重叠的不同变量上编写两个 for/end 循环,你不能编写自我修改的代码(或者不能以一种无缝的简单方式)等等。传统的程序员被上面的内容吓坏了,甚至不知道如何使用这些方法的力量,因为它们已经被提出来遵循传统的规则. 这是事实:今天我们的机器具有计算能力,可以做比我们使用它们的应用程序更多的事情,但是人脑无法在无规则的编码环境(=汇编)中对它们进行编码,并且需要限制性规则,这极大地减少频谱并简化编码。由于上述限制,我自己编写的代码无法用 C 代码编写而不会变得非常低效。而且我还没有谈到速度,大多数人认为这是在汇编中写作的主要原因,好吧,如果您的思想仅限于用 C 语言思考,那么您将永远是编译器的奴隶。我一直认为国际象棋大师是理想的汇编程序员,而 C 程序员只是玩“Dames”。
不再是速度,而是控制。速度有时来自控制,但这是在汇编中编码的唯一原因。其他所有原因都归结为控制(即 SSE 和其他手动优化、设备驱动程序和设备相关代码等)。
如果我能够胜过GCC和 Visual C++ 2008(也称为 Visual C++ 9.0),那么人们就会有兴趣采访我了解它是如何实现的。
这就是为什么目前我只是在汇编中阅读内容并在需要时只写 __asm int 3 。
我希望这有帮助...
我已经有几年没有写汇编了,但我以前的两个原因是:
我一直在看编码汇编,这无非是挑战和乐趣。我没有其他理由这样做:-)
我曾经接手过一个 DSP 项目,以前的程序员主要用汇编代码编写,除了用 C 语言编写的音调检测逻辑,使用浮点(在定点 DSP 上!)。音调检测逻辑以大约 1/20 的实时速度运行。
我最终从头开始重写了几乎所有内容。除了一些小的中断处理程序和几十行与中断处理和低级频率检测相关的代码外,几乎所有东西都在 C 中,它们的运行速度是旧代码的 100 倍以上。
我认为要记住的重要一点是,在许多情况下,使用小型例程来提高速度的机会比使用大型例程要大得多,特别是如果手写汇编程序可以将所有内容都放入寄存器但编译器不会相当管理。如果一个循环足够大以至于它无论如何都不能将所有内容保存在寄存器中,那么改进的机会就会少得多。
为 Android 手机上的 Java 应用程序解释字节码的 Dalvik VM 使用汇编程序作为调度程序。这部电影(大约 31 分钟,但值得观看整部电影!)解释了如何
“仍然存在人类比编译器做得更好的情况”。
我没有,但我已经指出至少要尝试,并在未来的某个时候努力尝试(希望很快)。当我使用高级语言进行编程时,了解更多低级内容以及幕后工作原理并不是一件坏事。不幸的是,作为开发人员/顾问和父母的全职工作很难获得时间。但我会在适当的时候放弃,这是肯定的。