如何在Assembly中多次浮动? 我在ebx中有一个值,并希望这个值为0.65
mov eax, ebx
mov ecx, 0x0.65 ;; how do this?
mul ecx
mov ebx, eax
答案 0 :(得分:4)
您是否希望将输入视为已签名或未签名?如果它已签名,转换为double
并返回实际上很简单快速(使用SSE2):在insn set ref中查找CVTSI2SD / MULSD / CVTSD2SI(x86标记wiki中的链接)。
它实际上可能比在现代CPU上使用整数IDIV更快,但可能比编译器技巧更慢,除以编译时常量100。
但是,由于你使用的是MUL,可能你的输入是无符号的,所以转换到double
实际上非常不方便,至少在32位机器上是这样。在x86-64上,您可以将32位整数零扩展为64位,然后将其视为64位有符号整数。
在http://gcc.godbolt.org/上放置一个C函数,然后查看asm输出(使用-O3)
re:哪个更快:
对于32位模式的天真整数方式,处理所有可能的无符号32位输入。 (不确定你是否可以更快地使用64位操作,因为div r/m64
比最近的英特尔CPU上的div r/m32
慢see Agner Fog's insn tables。)
# input in eax
mov ecx, 65
mul ecx # edx:eax = prod = eax*65
add eax, 50
adc edx, 0 # prod += 50
mov ecx, 100
div ecx # can't #DE because (65*0xFFFFFFFF+50)/100 is less than 2^32
# result in eax = 0.65 * input, sort of rounded to nearest but not quite.
mul r32
(4)+ add(1)+ adc(1)+ div r32
(26)= 32个周期。 请注意,从输入到输出的依赖关系链包括承载从ADD到ADC的数据依赖关系的EFLAGS。除了mov-immediate指令之外,没有并行性。 (那些可以用内存操作数替换,以减少融合域的uop计数,但这可能不是一个胜利)。
* SKL上的总uops:mov(1)+ mul r32
(3)+ add(1)+ adc(1)+ mov(1)+ div r32
(10:microcoded!)= 17个uops
*吞吐量:可能是DIV吞吐量的瓶颈,即SKL每6c一个(低于HSW每9c一个,SnB每个11-18c一个)。
FP方式,适用于x86-64。 (或者对x86-32上的有符号整数进行微小更改)。 double
可以精确地表示每个可能的整数32位整数,因此我们可以得到相同的结果。
# input in eax
mov edx, eax # zero-extend if you're not sure that the upper bits of rax were zero
cvtsi2sd xmm0, rdx
mulsd xmm0, [scale_factor]
cvtsd2si rax, xmm0
# result in eax = input * 0.65, rounded with the current SSE rounding mode (default = nearest)
section .rodata
scale_factor: dq 0.65
C编译器输出:
请参阅the Godbolt Compiler explorer上的source + asm输出。
简单的方法,不处理溢出,并使用技巧而不是DIV:
// 65*a can overflow
unsigned scale_int_32bit(unsigned a) {
return (a * 65U + 50) / 100;
}
# clang3.9 -m32 -O3 output
mov eax, dword ptr [esp + 4]
# input in eax
mov edx, 1374389535 # magic constant (modular multiplicative inverse of 100)
mov ecx, eax
shl ecx, 6 # 65 is 64 + 1
lea eax, [eax + ecx + 50] # eax = (a*65 + 50)
mul edx
shr edx, 5 # Do eax/100 with a multiplicative inverse
# result in edx
mov eax, edx
ret
这适用于32位,而FP方式没有。
mul r32
(4)+ shr(1)= 9 cycle 比FP方式更多uops,但延迟更低。吞吐量可能类似。
See this answer for info about lrint(x)
vs. (long)nearbyint(x)
:一些编译器用一个编译器做得更好,一些编译器更好地内联另一个编译器。
unsigned scale_fp(unsigned a) {
return (a * 0.65);
// nearbyint or lrint to get round-to-nearest,
// but in the asm it's mostly just cvt instead of cvtt.
// return lrint(a * 0.65);
}
# clang3.9 -O3 -m32 -msse2
.LCPI0_0:
.quad 4841369599423283200 # double 4503599627370496
.LCPI0_1:
.quad 4604029899060858061 # double 0.65000000000000002
.LCPI0_2:
.quad 4746794007248502784 # double 2147483648
scale_fp: # @scale_fp
movsd xmm0, qword ptr [.LCPI0_0] # xmm0 = mem[0],zero
movd xmm1, dword ptr [esp + 4] # xmm1 = mem[0],zero,zero,zero
orpd xmm1, xmm0
subsd xmm1, xmm0
movsd xmm0, qword ptr [.LCPI0_2] # xmm0 = mem[0],zero
mulsd xmm1, qword ptr [.LCPI0_1]
movapd xmm2, xmm1
cvttsd2si ecx, xmm1
subsd xmm2, xmm0
cvttsd2si eax, xmm2
xor eax, -2147483648
ucomisd xmm1, xmm0
cmovb eax, ecx
ret
正如您所看到的,将double
转换为最广泛的 unsigned 整数是非常糟糕的。 ICC和gcc使用略有不同的策略。我选择了clang的输出,因为它看起来更短,-fverbose-asm
提供了很好的注释来告诉你FP常量的double
值。
对于uint64_t
,此可能仍然比64位模式下的DIV更快(因为div r64
慢得多),但可能不适用于uint32_t
在32位模式下。 (虽然请注意double
无法准确表示每个uint64_t
。x87 fild
/ fistp
即使在32位模式下也会处理64位整数,并且80位内部FP表示具有64位尾数(因此它可以精确地表示每个int64_t
;但不确定uint64_t
。)
您可以通过使用-m32
编译而不使用-msse2
来查看此代码的x87版本。 Clang默认启用它,因此您可以使用-mno-sse2
。 (如果你不使用lrint或附近,那么舍入模式的改变会增加很多噪音。)
将输入转换为64位的版本的编译器输出很不幸。
unsigned scale_int_64bit(unsigned a) {
// gcc, clang and icc don't realize they can do this with one DIV,
// without risk of #DE, so they call __udivdi3
return (a * 65ULL + 50) / 100;
}
编译器调用libgcc函数进行64b / 64b除法,而不是使用DIV。我非常确定我的逻辑是正确的,并且64b / 32b = 32b DIV不会出错,因为我们如何相对于除数生成输入,因此商将适合32位。可能只是编译器无法证明这一点,或者没有一种模式来寻找机会来做到这一点。 __udivdi3
对上半部分进行了大量检查,因此它可能最终只能执行一个DIV
(但只有在显着分支之后)。