2016-09-13 42 views
0

我在读关于数学协处理器(Paul Carters PC Assembly Book)及其指令以进行浮点计算(在ASM i386上)。然后,我跑到下面的代码应该返回两个给定double值的大双(C调用公约):程序集(i386):数学协处理器堆栈

1 %define d1 ebp+8 
2 %define d2 ebp+16 
3 global dmax 
4  
5 segment .text 
6 dmax: 
7  enter 0,0 
8  
9  fld qword [d2] 
10  fld qword [d1] ;Now ST0 = d1 and ST1 = d2 
11  fcomip st1 ;Compares ST0 with ST1 and pops ST0 out 
12  jna short d2_bigger ;If not above (ST0<ST1) 
13  fcomp st0 ;Get rid of ST0, which is actually d2 now (line 11) 
14  fld qword [d1] 
15  jmp short exit 
16 d2_bigger: 
17 exit: 
18  leave 
19  ret 

有两件事情,我想这个代码改变。首先,我可能会使用FCOMI而不是FCOMIP来比较(第11行),以避免1个不必要的协处理器寄存器弹出。这样做,如果ST0 = ST1,根本就不会弹出(因为它已经在堆栈的顶部)。我看不到它的唯一原因是它会留下一堆非协处理器寄存器。但是,我认为C的唯一相关值是ST0,它将是双函数的返回值。如果另一个函数将多于8个浮点/双精度值推入协处理器堆栈,那么存储在协处理器堆栈(ST7)最低成员中的值是否会被丢弃?所以离开函数而不清除协处理器堆栈真的是一个问题吗? =>(READ编辑

我想改变的第二件事是我可能会不使用指令FCOMP第13行我明白它的原因有弹出ST0出栈,使ST1达到顶峰。但是,我认为做一个完整的比较并设置协处理器标志只是为了弹出这个值是一个开销。我查找了一条仅用于弹出ST0的指令,显然没有。我认为使用FADDP ST0, ST0(增加ST0到ST0并弹出ST0输出)或FSTP ST0(存储ST0到ST0的值并弹出ST0输出)会更快。他们只是看着我的头,就像对协处理器工作较少一样。

我试着测试3个选项(上面的代码,FSTP ST0FADDP ST0, ST0)的速度,经过几次快速测试后,它们都以非常相似的速度运行。从价值观中得出一个结论是不准确的。 显然FADDP ST0,ST0有点快,其次是FSTP ST0,最后是FCOMP ST0有推荐使用哪一个?或者,我是否过于担心会对整体速度产生如此微不足道的影响?

我刚刚质疑自己,因为由于大会是以尽可能最快的方式做事,所以也许选择其中一种方法会有好处。


编辑:

我正在读英特尔64和IA-32指令集和明显协处理器抛出异常如果堆栈溢出或下溢(例外#IS)。因此,使用堆栈并不清空它(在这种情况下,只留下ST0,因此C将弹出它的返回值)显然不是一个选项。

+2

世界正在迅速耗尽机器的编译,其中这种代码仍然有意义。特别是当你使用组装。改用SSE2代码。如果你不知道它是什么样子,请使用最新的C编译器。 –

+0

@HansPassant IIRC美国国家航空航天局一直在卫星上坚持旧的386-486 CPU。凭借其大晶体管,它们不易受宇宙射线变化的影响。但是,这是几年的信息,我不知道什么是目前的状态。 :)在其他地方,它可能是你写的,SSE2和更多的可用。 – Ped7g

+0

@ Ped7g:这使用FCOMI,它只在p6上可用。 –

回答

3

现代CPUs处理x87寄存器堆栈操作,类似于它们进行无序执行所需的寄存器重命名。 P版本的x87指令执行时的性能与非弹出版本相同。

对于需要静态分析现代CPU上此代码的延迟,吞吐量和总计uops的所有内容,请参阅Agner Fog's microarch guide and instruction tables。另外,tag wiki for more links

哦,绝对不会使用ENTER指令,除非在不关心速度的情况下完全针对大小进行优化。这是非常缓慢的,即使在0, 0的情况下。


平衡的FP堆栈:

抛出,如果堆栈溢出异常或下溢

FP异常默认情况下,在大多数操作系统所掩盖。行为更重要的部分是ST0在触发溢出的FLD之后保存垃圾。所以你的结论是正确的:按照x87堆栈的ABI规则是很重要的:在函数调用时堆栈为空,并在返回时清空或保存float/double返回值。 (我不知道,不同的事情任何的ABI的,但您可以在的x87寄存器传递一些FP ARGS不是堆栈的调用约定。)


C调用公约

在所有x86平台上都没有针对C的单一调用约定。许多32位的参数在栈上传递double参数,并将它们返回到ST(0)中,就像你正在做的那样。所以除了术语之外,这是可以接受的。

在通常的64位调用约定中,double参数在XMM寄存器(每个参数在其自己的寄存器的低位元素中)中传递。也有32位调用约定,假设SSE2并通过double这样。在这种情况下:

; 64-bit Windows or non-Windows, or 32-bit-with-double-in-SSE2 calling convention: 
global dmax 
section .text 
dmax: 
    maxsd xmm0, xmm1 
    ret 

没错,there's an instruction for std::max(double,double)。此时,函数调用的开销比指令多,并且使用asm函数而不是让C编译器将C函数内联到该指令是一个可怕的想法。特别是在所有XMM寄存器都被调用的调用约定(比如系统V,非Windows使用)中,因此调用者必须通过函数调用将所有临时数据保存/恢复到内存doublefloat


如果你必须用的x87指令写这篇

fcomp st0不只是弹出x87堆栈的最佳方式。使用fstp st0来做到这一点。

它看起来像你正在假设一个P6或更新的CPU(因为你使用FCOMI/FCOMIP),所以你也可以利用FCMOVcc,而不是使用分支。

; 32-bit args-on-the-stack 
section .text 
; when one input is NaN, might return NaN or might return the other input 
; This implements the C expression (d1 < d2) 
global dmax 
dmax: 
    fld  qword [esp+12] 
    fld  qword [esp+4]  ; ST0 = d1 and ST1 = d2 

    fucomi st0, st1 
    jp  handle_nan   ; optional. MAXSD does this for free. If you leave this out, I suggest using fcomi instead of fucomi, to raise #IA on NaN 
    FCMOVb st0, st1   ; st0 = (st0<st1) : st1 : st0. (Also copies if unordered because CF=1 in that case, too. But we don't know which operand was NaN.) 

    ;; our return value is in st0, but st1 is still in use. 
    fstp st1    ; pop the stack while keeping st0. (store it to st1, which becomes st0 after popping) 
    ; alternative: ffree st1 ; I think this should work 
    ret 

handle_nan: 
    faddp      ; add both args together to get a NaN, whichever one was NaN to start with. 
    ret 

这有一个非常可预测的分支(NaN可能永远不会发生在实际使用中,否则它总是会发生)。关键路径是通过内存的往返传输(〜5个周期),然后是fucomi(?) - > fcmov(2c) - > fstp st1(1c)。这些周期数是针对Intel Haswell的。总延迟=大概5 + 5(假设FUCOMI为2c)。

使用FFREE st1(如果有效的话)会使最终的fstp离开关键路径。 FXCHG(零延迟),然后弹出st0也可能将其从关键路径中取出。英特尔有可能实现像FXCHG这样零延迟的FSTP ST1(在寄存器重命名阶段处理),但我认为任何现有的微体系结构都不是这种情况。 (而且不太可能成为未来的特性,因为x87大多已经过时了,IIRC,Intel Skylake通过使更多的x87指令共享相同的执行端口,略微降低了一些x87的吞吐量。)

Intel Haswell吞吐量:Agner Fog的电子表格没有列出FUCOMI的延迟,但是它是3次。 FCMOV也是3个uops,有2个周期延迟。如果在预测得很好的情况下使用分支实现(在弹出st0之前有条件地运行FXCHG)可能会很好。无论如何,总UOP计数:

  • 2X FLD:2周的uop为端口2或PORT3
  • FUCOMI:3周的uop为P0/P1
  • JCC:1 UOP为P0/P6(假设预测-不可─取)
  • FCMOV:3个微指令(2P0 1P5)
  • FSTP REG:1个UOP为P0/P1
  • RET:1个UOP为P6(微熔合与P237负载这很有趣,我想P7仅适用于简单的商店地址,也许是表中的错字)

总融合域uops:10(不包括ret)。所以这需要2.5个周期(以4个为一组)。特定执行端口可能存在瓶颈,但我没有检查。


原来的gcc我的执行选择:)同意:

see the code on the Godbolt compiler explorer,用gcc6.2 -m32 -mfpmath=387 -O3 -march=haswell

double dmax(double a, double b) { return a<b ? b : a; } 

    fld  QWORD PTR [esp+4] 
    fld  QWORD PTR [esp+12] ;; it doesn't matter which order you load args in, IDK why I chose reverse order 
    fucomi st, st(1) 
    fcmovbe st, st(1)    ;; moving when they're equal matches the C, but of course doesn't matter 
    fstp st(1) 
    ret 
+0

对不起,需要一段时间来回复,我仍然在消化所有您提供的答案中的新信息!你确实向我表明,预测指令延迟并不像考虑它的作用那么简单,而且还考虑了许多CPU效率机制,如寄存器命名(我承认我不知道)。我会继续阅读和学习,直到我完全理解你的答案,但为了完整,并向我展示我接受它的更广泛的问题! – murphsghost