【问题标题】:Inline assembly vs math library内联汇编与数学库
【发布时间】:2014-03-10 03:58:04
【问题描述】:

高,谁能帮我理解为什么调用数学库函数比编写内联汇编代码来执行相同的操作更有效?我写了这个简单的测试:

#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 函数,另一种使用内联汇编代码。请注意,我会交替注释部分代码。 代码多次执行该操作以使用命令 time y 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 函数是通过多项式逼近和使用 sse 扩展来实现的。我想这会比 fptan 指令的微码更快。

【问题讨论】:

  • 你拆机看看有没有优化?
  • 也许它不能优化混合汇编和C。但是可以自己优化C并且忽略除了最后一个循环之外的所有内容。
  • 不需要优化编译器来计算您从未使用过的结果,因此无法保证除了最后一次之外正在执行该操作。但说到预期的问题,数学库的作者可能比你更擅长编写高效的汇编代码。
  • 我使用完全优化 (-O3) 编译了两个版本。我反汇编了这两个目标文件,我看到两个都存在循环。我在原始问题中附上了部分反汇编清单。
  • 请帮我理解汇编代码:根据 i,-0x4(%rbp) 计算的 ang,-0x8(%rbp) 的当前值究竟在哪里?还是优化编译器检测到 i/2000000 作为整数除法的结果始终为零,因此 ang=0 被加载为预先计算的常量?最好使用 M_PI/2000000*i。

标签: math gcc optimization assembly


【解决方案1】:

这些功能的实现有很大的不同。

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

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

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

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

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

话虽如此,您也可以尝试使用单个指令(例如 aam)实际上是否比使用经过优化的等效更简单指令集实现其内容更快。这是库的好处:它们可以使用 fptan 指令,但如果处理器架构支持一些更快的指令集、更多的并行性、更快的算法或所有这些,它们就不需要.

【讨论】:

  • 但根据 64-ia-architecture 手册,xmm 寄存器物理映射到 FPU 寄存器。
  • @user3396631:不,MMX 寄存器映射到 x87 堆栈。 MMX 与 x87-FPU 一样过时,但可以使用 SSE-SSE4/AVX/whatever-SIMD 的 XMM/YMM 寄存器。
  • FPU 寄存器和栈是一回事。另一方面,数学库似乎使用 MMX,因为操作数在调用 tanf 之前存储在 xmm0 寄存器中。
  • MMX 是第一个 SIMD 寄存器集,它映射到 ST(0)..(7) 以符合现有操作系统,后者必须在任务切换时保存/恢复 FPU。 XMM 是一组完全独立的寄存器,最初可通过“SSE”指令集获得。 MMX != XMM.
  • 仅供参考,MMX 寄存器被命名为 mm0 .. mm7.
【解决方案2】:

在这里(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() sn-p 周围(它被放置到局部变量中,并从那里使用)。我不是 x86_64 方面的专家,而且我的 GCC asm constraints-fu 已经生锈了……

(在任何情况下,您都必须减去整个 for 循环和计算角度。对于像这样的简单操作,这可能是总数的很大一部分)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-20
    • 2012-06-13
    • 1970-01-01
    • 2019-10-03
    • 1970-01-01
    • 2013-07-23
    • 2018-07-09
    • 2010-11-24
    相关资源
    最近更新 更多