已知的了解汇编器的原因之一是,有时可以用它来编写比用高级语言(尤其是C)编写更高性能的代码。 但是,我也听到过很多次声明,尽管这并非完全错误,但实际上可将汇编程序用于生成更多性能代码的情况极为罕见,并且需要汇编方面的专业知识和经验。
这个问题甚至都没有涉及到汇编程序指令将是特定于机器且不可移植的,或者汇编程序的任何其他方面。 当然,除了汇编语言之外,还有很多了解汇编语言的充分理由,但这是一个特定的问题,需要征集示例和数据,而不是对汇编语言和高级语言的扩展论述。
谁能提供一些特定的例子 , 说明使用现代编译器进行汇编比编写良好的C代码要快得多,并且您可以提供带有分析依据的主张吗? 我对这些案例的存在很有信心,但是我真的想确切地知道这些案例有多深奥,因为这似乎有些争议。
#1楼
根据我的经验,有几个例子:
访问无法从C访问的指令。例如,许多体系结构(如x86-64,IA-64,DEC Alpha和64位MIPS或PowerPC)支持64位乘64位乘法,产生128位结果。 GCC最近添加了扩展名,以提供对此类说明的访问,但是在需要该程序集之前。 当实施RSA之类的东西时,访问此指令可能对64位CPU产生巨大的影响-有时性能会提高4倍。
访问特定于CPU的标志。 咬住我很多的是进位标志; 在进行多精度加法运算时,如果您无法访问CPU进位,则必须比较结果以查看其是否溢出,这每条肢体需要3-5条指令; 更糟糕的是,就数据访问而言,这是串行的,这会破坏现代超标量处理器的性能。 当连续处理成千上万个这样的整数时,能够使用addc是一个巨大的胜利(进位位上的争用也存在超标量问题,但现代CPU处理起来很不错)。
SIMD。 即使是自动向量化的编译器也只能处理相对简单的情况,因此,如果要获得良好的SIMD性能,通常常常需要直接编写代码。 当然,您可以使用内部函数而不是汇编程序,但是一旦您进入内部函数级别,则基本上无论如何都在编写汇编程序,只是将编译器用作寄存器分配器和(名义上)指令调度程序。 (我倾向于将内在函数用于SIMD只是因为编译器可以为我生成函数序言,而不是为我生成函数序言,因此我可以在Linux,OS X和Windows上使用相同的代码而不必处理函数调用约定之类的ABI问题,但其他比SSE内在函数确实不是很好-Altivec的内在函数似乎更好,尽管我对它们没有太多经验。 作为一个事例,(当今)矢量化编译器无法弄清楚,请阅读有关位片化AES或SIMD纠错的信息 -可以想象一个编译器可以分析算法并生成这样的代码,但在我看来,这就像一个聪明的编译器距现有(至少)至少30年。
另一方面,多核计算机和分布式系统已将许多最大的性能优势转移到了另一个方向上-将内部循环以汇编形式编写可额外提高20%的速度,或者通过在多个内核上运行它们来实现300%的加速,或在10000%的速度下达到10000%在一组机器上运行它们。 当然,使用ML或Scala这样的高级语言比使用C或asm进行高级优化(诸如期货,备忘录等之类的东西)通常要容易得多,并且通常可以带来更大的性能优势。 因此,一如既往,需要进行权衡。
#2楼
简单的答案... 知道汇编的人 (又有他的参考,并且利用每一个小的处理器高速缓存和管道功能等)可以保证比任何编译器产生更快的代码。
但是,这些天的差异在典型应用中无关紧要。
#3楼
从汇编编码器的角度来看,C经常比您想像的要多做不必要的事情,因为C标准如此说。
例如,整数提升。 如果要在C中移动char变量,通常会希望代码实际上只是这样做,即一次移位。
但是,这些标准强制编译器在移位之前对int进行符号扩展,然后将结果截断为char,这可能会使代码复杂化,具体取决于目标处理器的体系结构。
#4楼
Longpoke,只有一个限制:时间。 如果您没有足够的资源来优化代码的每个更改,并花时间分配寄存器,优化少量溢出,而没有的话,则编译器将每次都获胜。 您对代码进行修改,重新编译和测量。 如有必要,请重复。
另外,您可以在高级方面做很多事情。 同样,检查生成的程序集可能会使IMPRESSION感觉到代码已被废弃,但实际上它的运行速度要比您认为的更快。 例:
int y = data [i]; //在这里做一些事情.. call_function(y,...);
编译器将读取数据,将其压入堆栈(溢出),然后从堆栈中读取并作为参数传递。 听起来很糟糕? 它实际上可能是非常有效的延迟补偿,并且可以加快运行时间。
//优化的版本call_function(data [i],...); //毕竟没有那么优化。
优化版本的想法是,我们减少了套准压力并避免了溢出。 但实际上,“糟糕”版本的速度更快!
查看汇编代码,仅查看说明并得出结论:更多的说明,较慢的说明将是错误的判断。
这里要注意的事情是:许多组装专家认为他们了解很多,但了解很少。 规则也从体系结构更改为下一个。 例如,没有银弹x86代码,它总是最快的。 这些天最好遵循经验法则:
- 记忆很慢
- 快取
- 尝试更好地使用缓存
- 你多久想念一次? 您有延迟补偿策略吗?
- 您可以为一个缓存未命中执行10-100条ALU / FPU / SSE指令
- 应用程序体系结构很重要。
- ..但是当问题不在体系结构中时它没有帮助
同样,过分相信编译器会神奇地将思想欠佳的C / C ++代码转换为“理论上最佳”的代码,这是一厢情愿的想法。 如果您在此低级关注“性能”,则必须了解所使用的编译器和工具链。
对于初学者来说,C / C ++中的编译器通常不太擅长重新排序子表达式,因为这些函数具有副作用。 函数式语言不会受到这种警告的困扰,但并不能很好地适应当前的生态系统。 有一些编译器选项允许宽松的精度规则,这些规则允许由编译器/链接器/代码生成器更改操作顺序。
这个话题有点死胡同。 对于大多数情况而言,这是无关紧要的,其余的,他们无论如何都知道自己在做什么。
归结为:“了解自己在做什么”,这与知道自己在做什么有些不同。
#5楼
我已经阅读了所有答案(超过30个),却找不到简单的原因:如果您已经阅读并练习了《 英特尔®64和IA-32架构优化参考手册》 , 那么汇编程序的运行速度会比C快。 更慢的是写这样慢的汇编的人没有看过“优化手册” 。
在Intel 80286的美好时光中,每条指令均以固定的CPU周期数执行,但是自1995年发布的Pentium Pro起,Intel处理器就利用了复杂流水线:乱序执行和寄存器重命名,成为了超标量。 在此之前,在1993年生产的Pentium上有U和V管线:双管线可以在不依赖时在一个时钟周期执行两条简单指令的情况; 但这与奔腾Pro中出现的乱序执行和寄存器重命名没有什么可比的,如今几乎保持不变。
用几句话来解释,最快的代码是指指令不依赖于先前的结果,例如,您应始终清除整个寄存器(通过movzx)或使用add rax, 1
或inc rax
来消除对标志先前状态的依赖等。 。
如果时间允许,您可以阅读更多关于乱序执行和注册重命名的信息,Internet上有很多可用信息。
还有其他重要问题,例如分支预测,加载和存储单元的数量,执行微操作的门的数量等,但是要考虑的最重要的事情是无序执行。
大多数人根本不了解乱序执行,因此他们像80286一样编写汇编程序,期望他们的指令将花费固定的时间来执行,而与上下文无关; 而C编译器知道乱序执行并正确生成代码。 这就是为什么这种不了解的人的代码速度较慢的原因,但是如果您意识到这一点,您的代码就会更快。
来源:oschina
链接:https://my.oschina.net/u/3797416/blog/3161240