内联汇编与数学库

时间:2014-03-08 17:57:13

标签: math gcc optimization assembly

很高,任何人都可以帮助我理解为什么调用数学库函数比编写内联汇编代码执行相同操作更有效?我写了这个简单的测试:

#include <stdio.h>
#define __USE_GNU
#include <math.h>

void main( void ){
    float ang;
    int i;

    for( i = 0; i< 1000000; i++){
        ang = M_PI_2 * i/2000000;
    /*__asm__ ( "fld %0;"
              "fptan;"
              "fxch;"
              "fstp %0;" : "=m" (ang) : "m" (ang)
    ) ;*/
    ang = tanf(ang);
    }
    printf("Tan(ang): %f\n", ang);
}

该代码以两种不同的方式计算角度的正切,一种从dinamically链接库libm.a调用tanf函数,另一种使用内联汇编代码调用tanf函数。请注意,我会替代地评论部分代码。 代码执行多次操作,以便在命令时间和linux终端获得有意义的结果。

使用数学库的版本大约需要0.040秒。 使用汇编代码的版本大约需要0.440秒;十倍以上。

这些是反汇编的结果。两者都使用-O3选项编译。

的libm

4005ad: b8 db 0f c9 3f          mov    $0x3fc90fdb,%eax
  4005b2:   89 45 f8                mov    %eax,-0x8(%rbp)
  4005b5:   f3 0f 10 45 f8          movss  -0x8(%rbp),%xmm0
  4005ba:   e8 e1 fe ff ff          callq  4004a0 <tanf@plt>
  4005bf:   f3 0f 11 45 f8          movss  %xmm0,-0x8(%rbp)
  4005c4:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  4005c8:   83 7d fc 00             cmpl   $0x0,-0x4(%rbp)
  4005cc:   7e df                   jle    4005ad <main+0x19>

ASM

40050d: b8 db 0f c9 3f          mov    $0x3fc90fdb,%eax
  400512:   89 45 f8                mov    %eax,-0x8(%rbp)
  400515:   d9 45 f8                flds   -0x8(%rbp)
  400518:   d9 f2                   fptan  
  40051a:   d9 c9                   fxch   %st(1)
  40051c:   d9 5d f8                fstps  -0x8(%rbp)
  40051f:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  400523:   83 7d fc 00             cmpl   $0x0,-0x4(%rbp)
  400527:   7e e4                   jle    40050d <main+0x19>

有什么想法吗?感谢。

我想我有个主意。浏览glibc代码我发现tanf函数是通过多项式aproximation并使用sse扩展来实现的。我想这比fptan指令的微码更快。

2 个答案:

答案 0 :(得分:4)

这些功能的实施存在重大差异。

fptan是使用浮点堆栈的 legacy 8087指令。即使最初的8087指令也是微编码的。调用fptan指令导致在8087 CPU中运行预定义程序,该程序将利用处理器的基本功能,例如浮点加法或甚至乘法。微码绕过“自然”流水线的某些阶段,例如,预取和解码,它加快了过程。

在8087中为三角函数选择的算法是CORDIC。

即使微编码比显式调用每条指令更快,但这并不是浮点处理器开发的结束;我们可以说,8087的开发已经结束了。在未来的处理器中, fptan 可能必须按原样实现,作为IP块,其行为与原始指令的行为相同,并且具有一些胶合逻辑,以便按原样生成逐位精确输出。 / p>

后来的处理器首先将FP堆栈重新用于“MMX”。然后引入了一组全新的寄存器(XMM)以及能够并行执行基本浮点运算的指令集(SSE)。首先,删除了对扩展精度浮点(80位)的支持。然后,20多年的摩尔定律允许分配更高的晶体管数量来构建,例如64x64位并行乘法器可加快乘法吞吐量。

其他说明也受到影响:loopsub ecx, 1; jnz组合快一倍。 aam今天可能比有条件地向eax的一些半字节添加10更慢 - 这些20多年的摩尔定律允许数百万个晶体管加速预取阶段:在8086中,指令编码中的每个字节都算作一个更多周期。今天有几条指令在一个周期内执行,因为指令已从内存中取出。

话虽这么说,你也可以试试,如果aam这样的单个指令实际上比使用一组更简单的指令实现它的内容更快, 优化。这是库的好处:他们可以使用fptan指令,但如果处理器架构支持更快的指令集,更多的并行性,更快的算法或所有这些指令,它们都不需要

答案 1 :(得分:0)

这里(Fedora 20,gcc-4.8.2-7.fc20.x86_64,Intel(R)Core(TM)i7-2670QM CPU @ 2.20GHz用-O2编译)我看到用户时间0.161s(asm)vs 0.076s(libm)。

虽然允许编译器摆脱库版本中的循环(知道 tanf(3m)是纯函数),但程序集显示循环在那里。并且函数没有内联,这是函数调用。还是更快。奇怪。

好吧,看起来差异是由于将参数混乱到asm()片段周围(它放在局部变量中,并从那里使用)。我不是x86_64的专家,我的GCC asm constraints-fu已经生锈了......

(无论如何,你必须减去整个for循环,并计算角度。对于这样的简单操作,这可能是总数的一个非常重要的部分。)