我很难使用内联汇编来击败我的编译器。
编译器很难真正、非常快速和简单地制作一个好的、非人为的函数示例是什么?但是用内联汇编来做这个相对简单。
如果您不考虑 SIMD 操作作弊,您通常可以编写性能比编译器自动矢量化能力更好的 SIMD 程序集(如果它甚至具有自动矢量化功能!)
这是一个非常基本的 SSE(x86 的 SIMD 指令集之一)教程。它适用于 Visual C++ 内联汇编。
编辑:如果您想自己尝试,这里有一小对功能。这是一个 n 长度点积的计算。一种是内联使用 SSE 2 指令(GCC 内联语法),另一种是非常基本的 C。
这非常简单,如果一个好的编译器不能向量化简单的 C 循环,我会感到非常惊讶,但如果不是,你应该会看到 SSE2 的加速。如果我使用更多寄存器,SSE 2 版本可能会更快,但我不想扩展我非常薄弱的 SSE 技能:)。
float dot_asm(float *a, float*b, int n)
{
float ans = 0;
int i;
// I'm not doing checking for size % 8 != 0 arrays.
while( n > 0) {
float tmp[4] __attribute__ ((aligned(16)));
__asm__ __volatile__(
"xorps %%xmm0, %%xmm0\n\t"
"movups (%0), %%xmm1\n\t"
"movups 16(%0), %%xmm2\n\t"
"movups (%1), %%xmm3\n\t"
"movups 16(%1), %%xmm4\n\t"
"add $32,%0\n\t"
"add $32,%1\n\t"
"mulps %%xmm3, %%xmm1\n\t"
"mulps %%xmm4, %%xmm2\n\t"
"addps %%xmm2, %%xmm1\n\t"
"addps %%xmm1, %%xmm0"
:"+r" (a), "+r" (b)
:
:"xmm0", "xmm1", "xmm2", "xmm3", "xmm4");
__asm__ __volatile__(
"movaps %%xmm0, %0"
: "=m" (tmp)
:
:"xmm0", "memory" );
for(i = 0; i < 4; i++) {
ans += tmp[i];
}
n -= 8;
}
return ans;
}
float dot_c(float *a, float *b, int n) {
float ans = 0;
int i;
for(i = 0;i < n; i++) {
ans += a[i]*b[i];
}
return ans;
}
由于它与 iPhone 和汇编代码有关,因此我将给出一个与 iPhone 世界相关的示例(而不是某些 sse 或 x86 asm)。如果有人决定为某个现实世界的应用程序编写汇编代码,那么这很可能是某种数字信号处理或图像处理。示例:转换 RGB 像素的色彩空间,将图像编码为 jpeg/png 格式,或将声音编码为 mp3、amr 或 g729 以用于 voip 应用程序。在声音编码的情况下,编译器无法将许多例程转换为高效的 asm 代码,它们在 C 中根本没有等效项。声音处理中常用的示例:饱和数学、乘法累加例程、矩阵乘法。
饱和加法示例:32 位有符号整数的范围:0x8000 0000 <= int32 <= 0x7fff ffff。如果添加两个整数,结果可能会溢出,但这在数字信号处理中的某些情况下可能是不可接受的。基本上,如果结果上溢或下溢饱和添加应返回 0x8000 0000 或 0x7fff ffff。那将是一个完整的 c 函数来检查它。饱和添加的优化版本可能是:
整数饱和添加(int a,int b) { 整数结果 = a + b; 如果 (((a ^ b) & 0x80000000) == 0) { if ((结果 ^ a) & 0x80000000) { 结果 = (a < 0) ? 0x80000000:0x7ffffff; } } 返回结果; }
您还可以执行多个 if/else 来检查溢出,或者在 x86 上您可以检查溢出标志(这也需要您使用 asm)。iPhone 使用具有 dsp asm 的 armv6 或 v7 cpu。因此,saturated_add
具有多个 brunch(if/else 语句)和 2 个 32 位常量的函数可能是一个仅使用一个 cpu 周期的简单 asm 指令。因此,简单地使 saturated_add 使用 asm 指令可以使整个算法快两三倍(并且尺寸更小)。这是 QADD 手册:
QADD
其他经常在长循环中执行的代码示例是
res1 = a + b1*c1; res2 = a + b2*c2; res3 = a + b3*c3;
似乎没有什么不能在这里优化,但是在 ARM cpu 上,您可以使用特定的 dsp 指令,这些指令比简单的乘法需要更少的周期!没错,带有特定指令的 a+b * c 可以比简单的 a*b 执行得更快。对于这种情况,编译器根本无法理解代码的逻辑,也不能直接使用这些 dsp 指令,这就是为什么你需要手动编写 asm 来优化代码,但你应该只手动编写一些确实需要的代码部分优化。如果您开始手动编写简单的循环,那么几乎可以肯定您不会击败编译器!网上有很多关于内联汇编的好论文,用于编码 fir 过滤器、amr 编码/解码等。
除非您是汇编专家,否则击败编译器的几率非常低。
来自上述链接的片段,
例如,面向位的“XOR %EAX, %EAX”指令是 x86 早期版本中将寄存器设置为零的最快方法,但大多数代码是由编译器生成的,而编译器很少生成 XOR 指令。因此,IA 设计者决定将频繁出现的编译器生成的指令移到组合解码逻辑的前面,使文字“MOVL $0, %EAX”指令的执行速度比 XOR 指令快。
我使用通用的“海峡 C”实现实现了一个简单的互相关。然后,当它花费的时间超过我可用的时间片时,我求助于算法的显式并行化并使用处理器内在来强制在计算中使用特定指令。对于这种特殊情况,计算时间从 >30ms 减少到刚刚超过 4ms。在下一次数据采集发生之前,我有一个 15 毫秒的窗口来完成处理。
这是 VLWI 处理器上的 SIMD 类型优化。这只需要 4 个左右的处理器内在函数,它们基本上是汇编语言指令,在源代码中给出函数调用的外观。您可以对内联汇编执行相同的操作,但语法和寄存器管理对于处理器内在函数来说要好一些。
除此之外,如果大小很重要,那么汇编程序就是王道。我和一个用不到 512 字节编写全屏文本编辑器的人一起上学。
我有一个校验和算法,它需要将单词旋转一定数量的位。为了实现它,我有这个宏:
//rotate word n right by b bits
#define ROR16(n,b) (((n)>>(b))|(((n)<<(16-(b)))&0xFFFF))
//... and inside the inner loop:
sum ^= ROR16(val, pos);
VisualStudio 发布版本扩展为:(val
在 ax 中,pos
在 dx 中,sum
在 bx 中)
mov ecx,10h
sub ecx,edx
mov ebp,eax
shl ebp,cl
mov cx,dx
sar ax,cl
add esi,2
or bp,ax
xor bx,bp
更有效的等效手工生成程序集将是:
mov cl,dx
ror ax,cl
xor bx,ax
我还没有弄清楚如何ror
从纯“c”代码发出指令。然而......
在写这篇文章时,我想起了编译器内在函数。我可以生成第二组指令:
sum ^= _rotr16(val,pos);
所以我的回答是:即使你认为你可以击败纯 c 编译器,在使用内联汇编之前检查内在函数。
如果你想做 SIMD 操作之类的事情,你可能会打败编译器。不过,这将需要对架构和指令集有很好的了解。
我对编译器的最大胜利是在一个简单的 memcpy 例程上......我跳过了很多基本的设置内容(例如,我不需要太多的堆栈帧,所以我在那里节省了几个周期),并且做了一些非常多毛的东西。
那是大约 6 年前的事了,当时有一些质量未知的专有编译器。我现在必须挖掘我拥有的代码并针对 GCC 进行尝试;我不知道它会变得更快,但我不会排除它。
最后,即使我的 memcpy 平均比我们 C 库中的快 15 倍,我还是把它放在我的后兜里,以备不时之需。它是我玩 PPC 组装的玩具,在我们的应用程序中速度提升不是必需的。