什么是简单的C函数的例子,它在内联汇编中实现得更快?

时间:2009-07-16 17:20:53

标签: assembly inline-assembly

我很难使用内联汇编来击败我的编译器。

一个好的,非人为的函数示例,编译器很难做到真正,非常快速和简单?但是使用内联汇编制作相对简单。

7 个答案:

答案 0 :(得分:8)

如果您不考虑SIMD操作作弊,您通常可以编写比编译器自动向量化能力更好的SIMD程序集(如果它甚至具有自动向量化!)

Here's一个非常基本的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;
}

答案 1 :(得分:7)

因为它与iPhone和汇编代码有关,所以我将给出一个与iPhone世界相关的例子(而不是某些sse或x86 asm)。 如果有人决定为某些真实世界的应用程序编写汇编代码,那么很可能这将是某种数字信号处理或图像处理。示例:转换RGB像素的颜色空间,将图像编码为jpeg / png格式,或将声音编码为mp3,amr或g729以用于voip应用程序。 在声音编码的情况下,有许多例程无法由编译器转换为有效的asm代码,它们在C语言中没有等价物。声音处理中常用的东西的例子:饱和数学,乘法累加例程,矩阵乘法。

饱和添加的示例:32位有符号的int具有范围:0x8000 0000&lt; = int32&lt; = 0x7fff ffff。如果添加两个整数,结果可能会溢出,但在某些情况下,这在数字信号处理中可能是不可接受的。基本上,如果结果溢出或下溢饱和,则应返回0x8000 0000或0x7fff ffff。这将是一个完整的c功能来检查。 饱和添加的优化版本可以是:

int saturated_add(int a, int b)
{
    int result = a + b;

    if (((a ^ b) & 0x80000000) == 0)
    {
        if ((result ^ a) & 0x80000000)
        {
            result = (a < 0) ? 0x80000000 : 0x7fffffff;
        }
    }
    return result;
} 

你也可以做多个if / else来检查溢出,或者在x86上你可以检查溢出标志(这也要求你使用asm)。 iPhone使用具有dsp asm的armv6或v7 cpu。因此,具有多个brunches(if / else语句)和2个32位常量的saturated_add函数可以是一个仅使用一个cpu周期的简单asm指令。 因此,简单地使饱和_add使用asm指令可以使整个算法快两到三倍(并且尺寸更小)。这是QADD手册: QADD

经常在长循环中执行的代码的其他示例是

res1 = a + b1*c1;
res2 = a + b2*c2;
res3 = a + b3*c3;

似乎没有什么不能在这里优化,但在ARM cpu上你可以使用特定的dsp指令,这些指令比简单的乘法花费更少的周期!没错,带有特定指令的+ b * c可以比简单的a * b执行得更快。对于这种情况,编译器根本无法理解代码的逻辑并且不能直接使用这些dsp指令,这就是为什么你需要手动编写asm来优化代码,但是你应该只手动编写需要的代码部分代码。优化。如果你开始手动编写简单的循环,那么几乎可以肯定你不会打败编译器! 网上有很多好的论文,用于内联汇编以编码fir过滤器,编码/解码等。

答案 2 :(得分:6)

除非你是assembly guru,否则击败编译器的几率非常低

来自上述链接的片段

  

例如,面向比特的“XOR   %EAX,%EAX“指令是   将寄存器设置为零的最快方法   在x86的早期阶段,   但大多数代码都是由   编译器和编译器很少   生成XOR指令。所以IA   设计师们,决定搬家了   经常出现的编译器   生成的指令直到前面   组合解码逻辑   使文字“MOVL $ 0,%EAX”   指令执行速度比   XOR指令。

答案 3 :(得分:5)

我使用通用的“strait C”实现实现了一个简单的互相关。然后,当它花费的时间超过我可用的时间片时,我采用了算法的显式并行化并使用处理器内在函数来强制在计算中使用特定指令。对于这种特殊情况,计算时间从> 30ms减少到刚好超过4ms。在下一次数据采集发生之前,我有一个15ms的窗口来完成处理。

这是VLWI处理器上的SIMD类型优化。这只需要4个左右的处理器内在函数,它们基本上是汇编语言指令,它们在源代码中提供函数调用的外观。您可以使用内联汇编执行相同的操作,但语法和寄存器管理对于处理器内在函数来说更好一些。

除此之外,如果尺寸重要,汇编程序就是王道。我和一个用不到512字节编写全屏文本编辑器的人去了学校。

答案 4 :(得分:5)

我有一个校验和算法,要求将字旋转一定的位数。为了实现它,我有这个宏:

//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

我还没想出如何从纯'c'代码发出ror指令。但是......
在编写本文时,我记得编译器内在函数。我可以用:

生成第二组指令
sum ^= _rotr16(val,pos);

所以我的答案是:即使你认为你可以击败纯c编译器,在使用内联汇编之前检查内在函数。

答案 5 :(得分:2)

如果你想做像SIMD操作这样的事情,你可能会击败编译器。这需要对架构和指令集有很好的了解。

答案 6 :(得分:0)

我对编译器的最好胜利是在一个简单的memcpy例程中...我跳过了很多基本设置的东西(例如,我不需要太多的堆栈帧,所以我在那里保存了几个周期) ,并做了一些非常多毛的东西。

那是大约6年前,有一些质量未知的专有编译器。我将不得不挖掘我的代码并立即尝试对抗GCC;我不知道它会变得更快,但我不会排除它。

最后,即使我的memcpy平均比我们的C库快了大约15倍,我还是把它放在我的后袋里以备不时之需。对我来说,玩PPC组件是一个玩具,在我们的应用中不需要提速。