现代CPUs处理x87寄存器堆栈操作,类似于它们进行无序执行所需的寄存器重命名。 P版本的x87指令执行时的性能与非弹出版本相同。
对于需要静态分析现代CPU上此代码的延迟,吞吐量和总计uops的所有内容,请参阅Agner Fog's microarch guide and instruction tables。另外,x86tag 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使用)中,因此调用者必须通过函数调用将所有临时数据保存/恢复到内存double
和float
。
如果你必须用的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
世界正在迅速耗尽机器的编译,其中这种代码仍然有意义。特别是当你使用组装。改用SSE2代码。如果你不知道它是什么样子,请使用最新的C编译器。 –
@HansPassant IIRC美国国家航空航天局一直在卫星上坚持旧的386-486 CPU。凭借其大晶体管,它们不易受宇宙射线变化的影响。但是,这是几年的信息,我不知道什么是目前的状态。 :)在其他地方,它可能是你写的,SSE2和更多的可用。 – Ped7g
@ Ped7g:这使用FCOMI,它只在p6上可用。 –