内联汇编 vs 数学库

5
高手们,能帮我理解为什么调用数学库函数比编写内联汇编代码执行相同操作更有效吗?我写了这个简单的测试:
#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);
}

这段代码以两种不同的方式计算角度的正切值,一种是调用动态链接库libm.a中的tanf函数,另一种是使用内联汇编代码。请注意,我会交替注释代码的部分。

该代码多次执行该操作,以在Linux终端中使用time命令获得有意义的结果。

使用math库的版本大约需要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函数是通过多项式逼近和使用sse扩展来实现的。我猜这比fptan指令的微码更快。


5
你是否已拆开它并查看是否进行了一些优化? - old_timer
1
也许它无法优化混合汇编和C语言。但是可以单独优化C语言并忽略除最后一个循环以外的所有内容。 - QuentinUK
我使用完全优化(-O3)编译了两个版本。我反汇编了这两个目标文件,发现循环在两个版本中都存在。我在原问题中附上了部分反汇编列表。 - pcremades
请帮我理解汇编代码:ang的当前值,-0x8(%rbp),是从i,-0x4(%rbp)计算得出的吗?还是优化编译器检测到i / 2000000始终为零,因为是整数除法的结果,所以ang = 0作为预先计算好的常量被加载了?最好使用M_PI / 2000000 * i。 - Lutz Lehmann
LutzL,你是对的,除法没有被计算。这是我的错误。无论如何,在两种情况下正切函数都被计算了一百万次。而且,是在-0x8(%rbp)中。 - pcremades
显示剩余5条评论
2个回答

4
这些函数的实现有很大的区别。
fptan是使用浮点堆栈的遗留8087指令。即使最初的8087指令也是微码化的。调用fptan指令会导致在8087 CPU中运行预定义的程序,该程序将利用处理器的基本功能,例如浮点加法甚至乘法。微码化绕过了一些“自然”的流水线阶段,例如预取和解码,并加快了进程速度。
8087中选择的三角函数算法是CORDIC。
尽管微码使fptan比显式调用每个指令更快,但这并不是浮点处理器发展的终点;我们可以说,8087开发已经结束了。在未来的处理器中,fptan可能必须按原样实现为一个IP块,其行为与原始指令完全相同,其中还包括一些胶水逻辑,以产生与原始输出完全一致的位。
后来的处理器首先将FP堆栈回收用于“MMX”。然后引入了一个全新的寄存器集(XMM),以及一个能够并行执行基本浮点操作的指令集(SSE)。首先,对扩展精度浮点数(80位)的支持被取消了。然后,20多年的摩尔定律允许分配更高的晶体管数量来构建例如64x64位并行乘法器,从而加速了乘法吞吐量。
其他指令也受到影响:loop曾经比sub ecx, 1; jnz组合更快。aam可能比今天有条件地将10添加到eax的某个nibble更慢--这20多年的摩尔定律已经允许数百万个晶体管加速预取阶段:在8086中,指令编码中的每个字节都计算为一个额外的周期。今天,由于指令已经从内存中获取,因此几个指令可以在单个周期内执行。
话虽如此,您还可以尝试单个指令(例如aam)是否比使用等效的一组更简单的优化指令实现其内容更快。这就是库的好处:它们可以使用fptan指令,但如果处理器架构支持一些更快的指令集、更多的并行性、更快的算法或所有这些,它们就不需要。

但是根据64位架构手册,xmm寄存器被物理映射到FPU寄存器。 - pcremades
@user3396631:不,MMX 寄存器被映射到 x87 栈。随着 SSE-SSE4/AVX/任何 SIMD 的 XMM/YMM 寄存器的出现,MMX 和 x87-FPU 一样过时了。 - EOF
FPU寄存器和堆栈是同一回事。另一方面,数学库似乎使用MMX,因为在调用tanf之前,操作数存储在xmm0寄存器中。 - pcremades
MMX是第一个SIMD寄存器集,它映射到ST(0)..(7)以符合现有操作系统的要求,在任务切换时必须保存/恢复FPU。XMM是一组完全独立的寄存器,最初通过"SSE"指令集提供。MMX != XMM。 - Aki Suihkonen
请注意,MMX寄存器的名称为_mm0_ .. _mm7_。 - Aki Suihkonen

0

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

虽然编译器可以在库版本中消除循环(它“知道”tanf(3m)是一个纯函数),但汇编显示循环仍然存在。而且该函数没有被内联,这里是一个函数调用。但速度更快。奇怪。

好吧,看起来差异是由于对asm()片段的参数进行了重新排序(将其放入局部变量中,并从那里使用)。我不是x86_64的专家,我的GCC asm约束-fu已经生锈了...

(无论如何,您都必须减去整个for循环和计算角度。对于像这样的简单操作,这可能是总数的相当大的一部分)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接