在装配中乘以浮点数

时间:2016-10-22 06:44:38

标签: assembly x86

如何在Assembly中多次浮动? 我在ebx中有一个值,并希望这个值为0.65

mov eax, ebx
mov ecx, 0x0.65 ;; how do this?
mul ecx
mov ebx, eax

1 个答案:

答案 0 :(得分:4)

您是否希望将输入视为已签名或未签名?如果它已签名,转换为double并返回实际上很简单快速(使用SSE2):在insn set ref中查找CVTSI2SD / MULSD / CVTSD2SI(标记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/m32see 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.
  • Skylake的延迟: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
  • Skylake的延迟:mov(0)+ cvt(6)+ mulsd(4)+ cvt(6)= 16个周期
  • SKL上的总uops:mov(1)+ cvt(2)+ mulsd(1)+ cvt(2)= 6 uops。
  • 吞吐量:可能每3个或者2个周期1个,IDK为什么CVTSI2SD的吞吐量为每2c一个,如果它真的只是p01的一个uop和p5的一个uop。也许它使用的是一个不完全流水线化的执行单元? Haswell列出的CVTSI2SD的吞吐量更差。

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方式没有。

  • SKL延迟:mov(0)+ shl(1)+ lea-with-3-components(3)+ 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(但只有在显着分支之后)。