关于C#:为什么要在汇编中编程?

Why do you program in assembly?

我有一个问题要问所有的底层黑客。我在博客里偶然发现了这句话。我并不认为消息来源很重要(如果你真的关心的话,那是哈克),因为这似乎是一个共同的声明。

For example, many modern 3-D Games have their high performance core engine written in C++ and Assembly.

就程序集而言——在程序集中编写代码是因为不希望编译器发出额外的指令或使用过多的字节,还是因为使用了更好的算法而无法用C语言表达(或者如果编译器不把它们弄乱就无法表达)?

我完全明白理解低级的东西很重要。我只想在您理解程序集后理解它的原因。


我认为你误解了这句话:

For example, many modern 3-D Games have their high performance core engine written in C++ and Assembly.

游戏(和现在的大多数程序)都不是"以汇编方式编写",就像"C++编写"一样。这个博客并不是说游戏的很大一部分是在汇编中设计的,也不是说一个程序员团队坐在汇编中作为他们的主要语言进行开发。

这真的意味着开发者首先编写游戏并让它在C++中运行。然后他们分析它,找出瓶颈是什么,如果值得的话,他们可以在组装中优化它们。或者,如果他们已经有了经验,他们知道哪些部分会成为瓶颈,并且他们已经从其他游戏中得到了优化的部分。

装配中的编程点和以前一样:速度。在汇编程序中编写大量代码是荒谬的,但是编译器还没有意识到一些优化,对于一个足够小的代码窗口,一个人会做得更好。

例如,对于浮点,编译器往往非常保守,可能不知道体系结构的一些更高级的特性。如果你愿意接受一些错误,你通常可以比编译器做得更好,如果你发现花了很多时间在代码上,那么在汇编中写这一小段代码是值得的。

下面是一些更相关的例子:

游戏示例

  • 英特尔关于使用SSE intrinsics优化游戏引擎的文章。最终的代码使用内部函数(不是内联汇编程序),因此纯汇编的数量非常少。但是,他们查看编译器输出的汇编程序,以准确地找出要优化的内容。

  • 地震的快速平方反比。同样,该例程中没有汇编程序,但是您需要了解一些关于体系结构的知识来进行这种优化。作者知道哪些操作速度快(乘法、移位),哪些操作速度慢(除法、sqrt)。因此他们提出了一个非常复杂的平方根实现,完全避免了缓慢的操作。

高性能计算

  • 在游戏领域之外,科学计算领域的人们经常对垃圾进行优化,使他们能够在最新的硬件上快速运行。把这当作你在物理上不能作弊的游戏。

    最近的一个很好的例子是晶格量子色动力学(晶格QCD)。本文描述了这个问题如何归结为一个非常小的计算内核,它在IBM Blue Gene/L上为PowerPC 440进行了大量优化。每个440都有两个FPU,并且它们支持一些特殊的三元操作,这些操作很难被编译器利用。如果没有这些优化,莱迪思QCD的运行速度会慢得多,当您的问题需要在昂贵的机器上花费数百万个CPU小时时,这是非常昂贵的。

    如果你想知道为什么这一点很重要,那就去看看这项工作的《科学》杂志上的文章。利用晶格QCD,这些人根据第一原理计算了质子的质量,并显示去年90%的质量来自强大的力结合能,其余的来自夸克。这就是E=mc2的作用。这是一个总结。

对于以上所有这些,应用程序都不是100%在汇编中设计或编写的——甚至不是很接近。但是,当人们真正需要速度时,他们专注于编写代码的关键部分,以便在特定的硬件上运行。


我已经多年没有用汇编语言编写代码了,但我可以给出我经常看到的几个原因:

  • 并非所有的编译器都能利用某些CPU优化和指令集(例如,英特尔偶尔添加的新指令集)。等待编译器编写者赶上意味着失去竞争优势。

  • 更容易将实际代码与已知的CPU体系结构和优化相匹配。例如,关于获取机制、缓存等您所知道的事情。这对开发人员来说应该是透明的,但事实是它不是透明的,这就是编译器编写器可以优化的原因。

  • 某些硬件级别的访问只能通过汇编语言(例如,在编写设备驱动程序时)实现。

  • 对于汇编语言来说,形式推理有时实际上比高级语言更容易,因为您已经知道代码的最终或几乎最终布局是什么。

  • 在缺少API的情况下,对某些3D图形卡进行编程(大约在20世纪90年代后期),通常在汇编语言中更为实用和高效,有时在其他语言中也不可能实现。但同样,这涉及到基于加速器体系结构的真正专家级的游戏,比如按一定顺序手动移动数据。

我怀疑很多人在使用高级语言的时候会使用汇编语言,特别是当这种语言是C语言的时候。手工优化大量通用代码是不切实际的。


汇编程序编程有一个其他人没有提到的方面——你知道应用程序中的每一个字节都是你自己努力的结果,而不是编译器的结果。我一秒钟也不想像80年代初那样用汇编程序编写整个应用程序,但我确实错过了那笔费用。玲有时候…


通常,一个外行的组装比C慢(由于C的优化),但许多游戏(我清楚地记得厄运)必须在组装中有特定的游戏部分,这样它才能在正常的机器上顺利运行。

这是我所指的例子。


我在第一份工作(80岁)就开始用汇编语言进行专业编程。对于嵌入式系统,内存需求(RAM和EPROM)很低。您可以编写紧凑的代码,这对资源来说很容易。

到了80年代后期,我已经切换到C语言。代码更容易编写、调试和维护。非常小的代码片段是用汇编程序编写的——对我来说,就是在编写上下文切换的时候,在一个Roll中使用您自己的RTO。(除非是一个"科学项目",否则你不应该再做了。)

您将在一些Linux内核代码中看到汇编程序片段。最近我在spinlocks和其他同步代码中浏览了它。这些代码需要访问原子测试和设置操作、操作缓存等。

我认为对于大多数通用编程来说,要优化现代C编译器是很困难的。

我同意@altcognito的观点,你最好把时间花在更认真地思考问题和做更好的事情上。由于某些原因,程序员经常关注微观效率,而忽视了宏观效率。汇编语言提高性能是一种微观效率。回退以获得更广泛的系统视图可能会暴露系统中的宏问题。解决宏观问题通常可以获得更好的性能收益。一旦宏观问题得到解决,然后崩溃到微观层面。

我想微问题是由一个程序员控制的,在一个较小的领域。在宏观层面上改变行为需要与更多的人交流——这是一些程序员所避免的。整个牛仔队和球队的较量。


"是的"。但是,要明白,在大多数情况下,用汇编程序编写代码的好处并不值得这么做。写在汇编中得到的回报往往比简单地集中精力更仔细地思考问题和花时间思考更好的方法来做偷窃要小。

John Carmack和Michael Abrash主要负责编写Quake以及所有进入IDS游戏引擎的高性能代码,在本书中详细介绍了这一点。

我也同意Lafur Waage的观点,即当今,编译器非常聪明,经常使用许多技术,利用隐藏的架构增强功能。


现在,至少对于顺序代码来说,一个好的编译器几乎总是胜过一个经验丰富的汇编语言程序员。但对于矢量码来说,这是另一回事。例如,广泛部署的编译器并不能很好地利用X86SSE单元的向量并行功能。我是一个编译器编写者,利用SSE是我列出的自行开发而不是信任编译器的原因。


SSE代码在汇编中比编译器内部函数工作得更好,至少在MSVC中如此。(即不创建额外的数据副本)


一些指令/标志/控件根本不在C级别。

例如,检查x86上的溢出是简单的溢出标志。此选项在C中不可用。


缺陷往往是每行运行(语句、代码点等);虽然对于大多数问题来说,汇编使用的行比更高级别的语言要多得多,但偶尔也会出现这样的情况:它是当前问题的最佳映射(最简洁、最少的行)。这些案件大多涉及到常见的嫌疑犯,比如驱动程序和嵌入式系统中的比特碰撞。


我在工作时的源代码中有三到四个汇编程序例程(在大约20MB的源代码中)。所有这些都是SSE(2),并且与操作(相当大-考虑2400x2048和更大)图像相关。

为了业余爱好,我研究一个编译器,在那里你有更多的汇编程序。运行时库中经常充满了它们,其中大多数都与那些违背正常过程机制的东西有关(比如异常的助手等)。

我的微控制器没有汇编程序。大多数现代微控制器都有很多外围硬件(中断控制计数器,甚至整个正交编码器和串行积木),因此使用汇编程序来优化循环通常不再需要。对于当前的闪存价格,代码存储器也是如此。此外,还经常有一系列与管脚兼容的设备,因此,如果系统性地耗尽CPU电源或闪存空间,则升级通常不是问题。

除非你真的运送了100000台设备和编程汇编程序,否则只要把闪存芯片装在一个更小的类别中,就可以真正节省大量成本。但我不属于这个范畴。

很多人认为嵌入式是汇编程序的一个借口,但是他们的控制器比Unix开发的机器有更多的CPU能力。(微芯片来了40和60 mips微控制器,价格低于10美元)。

然而,很多人都被传统所束缚,因为改变微芯片的结构并不容易。此外,HLL代码与体系结构非常相关(因为它使用硬件外围设备、寄存器来控制I/O等)。所以有时有很好的理由继续在汇编程序中维护一个项目(我很幸运能够从头开始在一个新的体系结构上设置事务)。但人们常常自欺欺人地说他们真的需要装配工。

我仍然喜欢教授在我们问我们是否可以使用goto时给出的答案(但你也可以把它当作汇编程序来读):"如果你认为值得写一篇3页的文章来说明你为什么需要这个特性,你可以使用它。请提交论文并附上你的成绩。"

我把它作为底层特性的指导原则。不要太狭隘而不能使用它,但要确保你正确地激励它。甚至设一两道人为的障碍(如文章),以避免用复杂的推理作为理由。


我认为很多游戏开发者会对这些信息感到惊讶。

我知道的大多数游戏都是尽可能少的组装。在某些情况下,根本没有,最坏的情况是,一个或两个循环或函数。

这句话太笼统了,没有像十年前那样准确。

但是,仅仅是事实不应该妨碍真正的黑客支持组装的运动。;)


如果您正在用128字节的RAM和4K的程序内存对低端8位微控制器进行编程,那么您就没有太多选择来使用汇编。有时,当使用一个更强大的微控制器时,你需要在一个准确的时间采取某种行动。汇编语言很有用,因为您可以计算指令,从而测量代码使用的时钟周期。


另一个原因可能是,当可用的编译器不足以满足体系结构的需要,并且程序中所需的代码量没有程序员在其中迷失的时间长或复杂。尝试为一个嵌入式系统编程一个微控制器,通常组装会容易得多。


除此之外,所有高级语言都有一定的局限性。这就是为什么有些人选择在ASM中编程,以完全控制他们的代码。

其他人喜欢非常小的可执行文件,范围在20-60kb,例如,由hiedit控件的作者实现的check hieditor,它是一个非常强大的编辑控件,用于语法突出显示的窗口和仅约50kb的选项卡)。在我的收藏中,我有超过20个这样的黄金控件,从Excel(如ssheets)到HTML呈现。


如果你参与了所有2000年的修复工作,如果你了解组装,你可能会赚很多钱。其中仍然有大量的遗留代码,这些代码偶尔需要维护。


除了在非常小的CPU上进行非常小的项目外,我不会着手在汇编中编程整个项目。然而,人们通常会发现,通过对一些内部循环进行战略性手工编码,可以缓解性能瓶颈。

在某些情况下,真正需要做的就是用一条指令替换一些语言结构,而优化器不可能理解如何使用该指令。一个典型的例子是在DSP应用程序中,向量运算和乘法累加运算对于优化器来说很难发现,但易于编写代码。

例如,SH4的某些模型包含4x4矩阵和4个向量指令。我看到了颜色校正算法的巨大性能改进,用适当的指令替换3x3矩阵上的等效C操作,而将校正矩阵扩大到4x4以匹配硬件假设的微小成本。这是通过编写不超过12行的汇编代码,对相关数据类型进行匹配调整,并将其存储到周围C代码的少数位置来实现的。


上次我用汇编程序编写代码时,我不能说服编译器生成无libc、位置无关的代码。

下一次可能也是出于同样的原因。

当然,我曾经有过其他a href="http://linuxassembly.sourceforge.net">原因/a a。


到目前为止,我所看到的几乎每一个中型到大型的游戏引擎或库都有一些手工优化的装配版本可用于矩阵操作,如4x4矩阵连接。在处理大型矩阵时,编译器似乎不可避免地会错过一些巧妙的优化(重用寄存器、以最高效的方式展开循环、利用特定于机器的指令等)。这些矩阵操作函数几乎总是配置文件上的"热点"。

我还看到手工编码的程序集在定制调度中使用了很多——比如fastdelegate,但是编译器和特定于机器的。

最后,如果您有中断服务例程,ASM可以使世界上所有的不同——有一些您不希望在中断下发生的操作,并且您希望您的中断处理程序"快速进入和退出"…如果ISR是在ASM中,你几乎完全知道它会发生什么,它鼓励你保持血腥的事情简短(无论如何这是一个好的实践)。


我只是亲自和一个开发人员谈过他对组件的使用。他正在研究处理便携式MP3播放器控制的固件。在装配中进行工作有两个目的:

  • 速度:需要最小的延迟。
  • 成本:通过最小化代码,运行它所需的硬件功能可能会稍差一些。当大量生产数百万单位时,这可以加起来。

  • 游戏非常需要性能,虽然优化器很好,"主程序员"仍然能够通过手工编码组件中的正确部分来挤出更多的性能。

    永远不要在不分析程序的情况下开始优化程序。分析之后,应该能够识别瓶颈,如果找到更好的算法之类的东西再也不能解决问题,您可以尝试在汇编中手工编写一些东西。


    我继续做的唯一一个汇编程序编码是针对资源不足的嵌入式硬件。正如Leander所提到的,assembly仍然非常适合于ISR,在ISR中,代码需要快速且易于理解。

    我的第二个原因是保持我对装配功能的了解。能够检查和理解CPU为完成我的出价所采取的步骤感觉很好。


    很多人喜欢贬低汇编语言,因为他们从未学过用汇编语言编写代码,只是含糊其辞地遇到过汇编语言,而汇编语言让他们要么惊呆了,要么有点吓人。真正有天赋的程序员会明白,抨击C或汇编是毫无意义的,因为它们是免费的。事实上,一方的优势是另一方的劣势。C的有组织的语法规则提高了清晰度,但同时放弃了所有的动力总成,从没有任何结构规则!C代码指令是用来创建非阻塞代码的,可以说这有助于明确编程意图,但这是一种功耗。在C语言中,编译器不允许在if/elseif/else/end中跳转。或者,不允许在相互重叠的不同变量上编写两个for/end循环,也不能编写自修改代码(或不能以无缝、简单的方式)等。传统的程序员被上述情况吓坏了,他们甚至不知道如何使用这些方法的力量,因为它们被提升为遵循传统规则。事实是:今天,我们有了一台具有计算能力的机器,可以做更多的工作来完成我们使用它们的应用程序,但是人类的大脑无法在无规则的编码环境(即汇编)中对它们进行编码,并且需要限制性的规则来大大减少频谱并简化编码。我自己编写的代码不能用C代码编写,而不会因为上述限制而变得非常低效。我还没有谈到大多数人认为写汇编的主要原因是速度,如果你的思想局限于C语言,那么你永远是编译器的奴隶。我一直认为国际象棋大师是理想的汇编程序设计员,而C程序设计员只是玩"dames"。


    它似乎没有被提到,所以我想补充一下:在现代游戏开发中,我认为至少正在编写的一些程序集根本不适合CPU。它是为GPU,以着色程序的形式。

    这可能是出于各种原因所需要的,有时仅仅是因为所使用的更高级的着色语言不允许精确的操作以所需的指令数量表示,以适应某些大小限制、速度或任何组合。就像汇编语言编程一样,我猜。


    我已经有几年没有在大会上写作了,但我以前写的两个原因是:

    • 这件事的挑战!我经历了几个月的时间以前我在x86程序集中编写所有内容(DOS和Windows时代3.1)。它基本上教会了我大量的低级操作、硬件I/O等。
    • 在某些情况下,它保持较小的大小(写TSR时,同样是DOS和Windows3.1)

    我一直在看代码汇编,这只不过是一件事的挑战和乐趣。我没有其他理由这样做:—)


    不再是速度,而是控制。速度有时来自控制,但它是在程序集中进行代码编写的唯一原因。其他原因归结为控制(即SSE和其他手优化、设备驱动程序和设备相关代码等)。


    我曾经接手过一个DSP项目,之前的程序员主要是用汇编代码编写的,除了用C语言编写的音调检测逻辑,使用浮点(在定点DSP上!).音调检测逻辑以大约1/20的实时运行。

    最后我几乎重写了所有的东西。几乎所有的代码都是C语言,除了一些小的中断处理程序和一些与中断处理和低级别频率检测相关的代码行,这些代码的运行速度比旧代码快100倍以上。

    我认为,要记住的一件重要的事情是,在许多情况下,使用小程序进行速度增强的机会要比使用大程序大得多,特别是如果手工编写的汇编程序可以将所有内容都放入寄存器中,但编译器无法完全管理的话。如果一个循环足够大以至于它不能把所有的东西都保存在寄存器中,那么改进的机会就少得多。


    如果我能超越GCC和Visual C++ 2008(也被称为Visual C++ 9),那么人们会有兴趣采访我关于它是如何可能的。

    这就是为什么现在我只是在汇编中读东西,在需要的时候只写"asm int 3"。

    我希望这个帮助…


    在Android手机上解释Java应用程序字节码的Dalvik VM使用调度器的汇编器。这部电影(大约31分钟,但值得看整部电影!)解释如何

    "there are still cases where a human can do better than a compiler".


    我没有,但我已经指出至少要尝试,并在未来的某个阶段努力尝试(希望很快)。当我用高级语言编程时,了解更多低级的东西,以及后台的工作原理,并不是一件坏事。不幸的是,作为一名开发人员/顾问和一名家长,很难找到全职工作。但我会在适当的时候放弃,这是肯定的。