什么时候装配比C快?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么时候装配比C快?相关的知识,希望对你有一定的参考价值。
了解汇编程序的一个原因是,有时可以使用它来编写比使用更高级语言编写代码更高效的代码,特别是C.但是,我也听过很多次说虽然这并非完全错误,但汇编程序实际上可用于生成更高性能代码的情况极为罕见,需要专业知识和汇编经验。
这个问题甚至没有涉及汇编程序指令将是机器特定的和不可移植的,或汇编程序的任何其他方面的事实。当然,除了这一点之外,还有很多很好的理由知道汇编,但这是一个特定的问题,征求例子和数据,而不是关于汇编语言与高级语言的扩展讨论。
任何人都可以提供一些特定的例子,其中汇编将比使用现代编译器的编写良好的C代码更快,并且您是否可以通过分析证据来支持该声明?我非常有信心这些案例存在,但我真的想知道这些案件究竟有多深奥,因为这似乎是一些争论的焦点。
这是一个真实世界的例子:旧编译器上的定点乘法。
这些不仅可以在没有浮点的设备上得心应用,它们在精度方面也会发光,因为它们可以提供32位精度并且具有可预测的误差(浮点数仅为23位,并且难以预测精度损失)。即在整个范围内均匀的绝对精度,而不是接近均匀的相对精度(float
)。
现代编译器很好地优化了这个定点示例,因此对于仍需要编译器特定代码的更现代的示例,请参阅
- Getting the high part of 64 bit integer multiplication:使用
uint64_t
进行32x32 => 64位乘法的便携式版本无法在64位CPU上进行优化,因此您需要内部函数或__int128
才能在64位系统上实现高效代码。 - _umul128 on Windows 32 bits:当将32位整数乘以64时,MSVC并不总是做得很好,因此内在函数帮助很大。
C没有全乘法运算符(N位输入的2N位结果)。在C中表达它的通常方法是将输入转换为更宽的类型,并希望编译器识别出输入的高位不感兴趣:
// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
long long a_long = a; // cast to 64 bit.
long long product = a_long * b; // perform multiplication
return (int) (product >> 16); // shift by the fixed point bias
}
这段代码的问题在于我们做了一些无法用C语言直接表达的东西。我们想要将两个32位数相乘并获得64位结果,其中我们返回中间的32位。但是,在C中,这种乘法不存在。你所能做的就是将整数提升到64位并进行64 * 64 = 64乘法运算。
但是,x86(以及ARM,MIPS和其他设备)可以在单个指令中进行乘法运算。一些编译器过去忽略了这个事实并生成了调用运行时库函数来执行乘法的代码。 16的转换通常也是由库例程完成的(x86也可以进行这样的转换)。
所以我们只剩下一个或两个库调用来进行乘法运算。这会产生严重后果。不仅移位速度较慢,还必须在函数调用中保留寄存器,它也无助于内联和代码展开。
如果在(内联)汇编程序中重写相同的代码,则可以获得显着的速度提升。
除此之外:使用ASM不是解决问题的最佳方法。如果你不能用C语言表达它们,大多数编译器允许你使用内部形式的一些汇编指令。例如,VS.NET2008编译器将32 * 32 = 64位mul公开为__emul,64位移位为__ll_rshift。
使用内在函数,您可以以C编译器有机会了解正在发生的事情的方式重写函数。这允许代码内联,寄存器分配,公共子表达消除和常量传播也可以。与那种手写的汇编程序代码相比,你将获得巨大的性能提升。
供参考:VS.NET编译器的定点mul的最终结果是:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
固定点分割的性能差异更大。通过编写几个asm-lines,我对分区重定点代码进行了10倍的改进。
使用Visual C ++ 2013为两种方式提供相同的汇编代码。
2007年的gcc4.1也很好地优化了纯C版本。 (Godbolt编译器浏览器没有安装任何早期版本的gcc,但可能更老的GCC版本可以在没有内在函数的情况下执行此操作。)
请参阅x86(32位)的source + asm和the Godbolt compiler explorer上的ARM。 (不幸的是,它没有足够的编译器来生成简单纯C版本的错误代码。)
现代CPU可以做C根本没有运算符的东西,比如popcnt
或bit-scan来找到第一个或最后一个设置位。 (POSIX有一个ffs()
函数,但它的语义与x86 bsf
/ bsr
不匹配。请参阅https://en.wikipedia.org/wiki/Find_first_set)。
有些编译器有时可以识别一个循环来计算整数中的设置位数并将其编译为popcnt
指令(如果在编译时启用),但在GNU C中使用__builtin_popcnt
或在x86上使用它更可靠。仅针对SSE4.2的硬件:_mm_popcnt_u32
from <immintrin.h>
。
或者在C ++中,分配给std::bitset<32>
并使用.count()
。 (这种情况下,语言已经找到了一种方法,可以通过标准库轻松暴露popcount的优化实现,以一种始终编译为正确的方式,并且可以利用目标支持的任何内容。)另请参阅https://en.wikipedia.org/wiki/Hamming_weight#Language_support 。
类似地,ntohl
可以在具有它的某些C实现上编译为bswap
(x86 32位字节交换以进行字节序转换)。
内在函数或手写asm的另一个主要领域是使用SIMD指令的手动矢量化。像dst[i] += src[i] * 10.0;
这样的简单循环编译器也不错,但是当事情变得更复杂时,编程通常会很糟糕或者根本不会自动矢量化。例如,你不太可能得到像编译器从标量代码自动生成的How to implement atoi using SIMD?。
第一点不是答案。 即使你从来没有编程,我发现至少知道一个汇编指令集很有用。这是程序员永远不断寻求了解更多并因此更好的一部分。在进入框架时也很有用,你没有源代码,并且至少知道发生了什么。它还可以帮助您理解JavaByteCode和.Net IL,因为它们与汇编程序类似。
当您有少量代码或大量时间时回答问题。最适用于嵌入式芯片,其中低芯片复杂性和针对这些芯片的编译器的竞争不足可能会使人们有利于平衡。此外,对于受限设备,您通常会以难以指示编译器执行的方式处理代码大小/内存大小/性能。例如我知道这个用户操作不经常被调用,所以我的代码大小很小,性能很差,但是这个看起来很相似的其他函数每秒都会被使用,所以我将拥有更大的代码大小和更快的性能。这是熟练的汇编程序员可以使用的那种权衡。
我还想补充一下,有很多中间地带你可以用C编译代码并检查生成的汇编,然后改变你的C代码或调整和维护为汇编。
我的朋友在微控制器上工作,目前用于控制小型电动机的芯片。他的工作是低级别c和汇编。他曾经告诉我工作中的好日子,他将主要循环从48条指令减少到43条。他还面临着代码已经增长到填充256k芯片的选择,业务需要一个新功能,你呢?
- 删除现有功能
- 减少部分或全部现有功能的大小可能会以性能为代价。
- 倡导转向更大的芯片,具有更高的成本,更高的功耗和更大的外形尺寸。
我想作为一个商业开发人员添加一个或多种语言,平台,类型的应用程序,我从来没有觉得有必要深入编写程序集。我一直都很欣赏我所获得的知识。有时调试到它。
我知道我已经回答了“我为什么要学习汇编程序”这个问题,但是我认为这是一个更重要的问题,那么它什么时候会更快。
所以让我们再试一次你应该考虑装配
- 致力于低级操作系统功能
- 在编译器上工作。
- 在极其有限的芯片,嵌入式系统等上工作
请记住将您的程序集与生成的编译器进行比较,以查看哪个更快/更小/更好。
大卫。
我很惊讶没人说这个。如果用汇编语言编写,strlen()
函数会快得多!在C中,你能做的最好的事情就是
int c;
for(c = 0; str[c] != ' '; c++) {}
在装配时你可以大大加快速度:
mov esi, offset string
mov edi, esi
xor ecx, ecx
lp:
mov ax, byte ptr [esi]
cmp al, cl
je end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp
end_4:
inc esi
end_3:
inc esi
end_2:
inc esi
end_1:
inc esi
mov ecx, esi
sub ecx, edi
长度是ecx。这比较了4个字符,因此速度提高了4倍。并且考虑使用eax和ebx的高阶词,它将比之前的C例程快8倍!
我不能给出具体的例子,因为它是在很多年前,但是有很多情况下手写汇编程序可以胜过任何编译器。原因:
- 您可以偏离调用约定,在寄存器中传递参数。
- 您可以仔细考虑如何使用寄存器,并避免将变量存储在内存中。
- 对于像跳转表这样的东西,你可以避免必须检查索引。
基本上,编译器在优化方面做得非常好,并且几乎总是“足够好”,但在某些情况下(如图形渲染),你需要为每个周期付出高昂的代价,你可以采用
以上是关于什么时候装配比C快?的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向Android 进程注入工具开发 ( Visual Studio 开发 Android NDK 应用 | Visual Studio 中 SDK 和 NDK 安装位置 )(代码片段