x86 machine code, 14 bytes
Same machine code works in 32 or 64-bit mode. (Same length for 16-bit mode would be easy with different regs). Callable from C as x86-64 System V int8_t getexp(const char *b2_str);
23 ; input: char *str in RDI
24 ; return: int8_t AL; (calling convention allows garbage in high bytes of RAX)
25 getexp_scalar:
26 00000030 6A09 push 9 ; sign bit gets shifted out the top.
27 00000032 59 pop rcx
28 00000033 B030 mov al, '0'
29 .loop:
30 00000035 AE scasb ; set FLAGS like cmp '0', [rdi] would, if it was encodeable
31 00000036 10D2 adc dl,dl ; DL = DL<<1 | CF, rotate CF into DL.
32 00000038 E2FB loop .loop
33 0000003A 8D4281 lea eax, [rdx-127] ; no shorter than sub dl, 127 or xchg eax,edx / sub al,127
34 0000003D C3 ret
We just do base 2 ASCII->integer conversion for 9 bits, letting the first bit (sign bit) overflow off the top of DL. The 8-bit return value is only the 8-bit AL register, as a 2's complement binary integer.
adc dl,dl
to rotate CF into DL is more efficient on modern CPUs than rcl dl, 1
(rotate through carry, i.e. shift in CF). They're equivalent except for some FLAGS output semantics. I could have used EDX for the same code size; that would simply leave different garbage in the high bytes outside the AL return value in RAX. (And yes, high garbage above narrow return values is 100% allowed by standard x86 calling conventions.)
Try it online! 3 versions with a test caller.
(Including alternate 15-byte scalar version using lodsb
/ and al, 1
/ lea edx, [rdx*2 + rax]
)
x86-64 SIMD (MMXext+MOVBE) machine code, 20 bytes
Having base-2 digits in MSB-first printing order is a major inconvenience for little-endian x86, where the lowest SIMD vector element comes from the lowest address in memory. x86 lacks a bit-reverse like ARM's rbit
, so I used x86-64 features to byte-reverse 8 bytes. Without that, movq mm0, [e/rdi+1]
(4B) / pslld mm0,7
(4B) would be 32-bit-mode compatible and work on a Pentium III (pmovmskb r32, mm
was new with SSE, aka MMXext, so not original P5 Pentium-MMX).
; input: char *RDI; result int8_t AL
; clobbers: MM0. Leaves FPU in MMX state (no EMMS)
12 getexp_mmx:
14 00000010 480F38F04701 movbe rax, [rdi+1] ; byte-reverse (big-endian) load of the exponent field. saves 1B vs. mov + bswap
15 00000016 48C1E007 shl rax, 7 ; low bit to high in each byte. same size as pslld mm0, 7.
16 0000001A 480F6EC0 movq mm0, rax
17 0000001E 0FD7C0 pmovmskb eax, mm0 ; pack vector high bits into 1 byte
18 00000021 2C7F sub al, 127
19 00000023 C3 ret
; size = 0x24 - 0x10 = 0x14 = 20 B
MMX instructions typically need 1 fewer prefix byte than SSE2 and later; if you were doing this for real you'd probably use XMM0, and avoid the EMMS (exit MMX state) that real code would normally use. (And maybe a pshufb
for the byte reverse if you have SSSE3, to avoid bouncing through integer regs...) This should still be much faster than a loop with adc and false dependencies, and the slow loop
instruction itself. It's handy that the exponent field is exactly 8 bits wide. Doing this for double
would need an SSE2 16-byte load (or 2 byte reverses and annoying shuffles to combine into a 16-byte vector), and would have to mask away the sign bit.
lodsb (1) + lodsq (2) + bswap rax (3) is the same total length as movbe rax, [rsi+1]
(6), although that would make this portable to machines without movbe
(Atom, Haswell and later).
For subnormal (aka denormal) floats including 0.0
, this just returns -127
, the unbiased the exponent field, as requested by the question. And -128
(overflowed from +128) for Inf / NaN inputs. (I only realized this overflow issue after the fact, posting anyway because all 255 possible outputs are unique, and that puts both special exponents at adjacent values so the caller can test for subnormal or inf/nan with e <= -127
.)
It doesn't return the actual floor(log2(|x|))
of the represented value like AVX512 vgetexpps xmm, xmm
does: (-Inf
for an input of 0, and values below -127 for non-zero subnormals, matching the pseudocode in the documentation).
So even if we could take input as an actual binary floating-pointer number in IEEE754 binary32 format (aka float
) in an XMM register, we couldn't just use that one 6-byte AVX512 instruction to produce an integer-valued float
result.
not competing AVX2 version, 15 bytes
This takes its arg as 32 bytes of ASCII digits in a YMM register, but they have to be in least-significant-digit first order, hence not competing.
2 getexp_avx2_ymm_reversed:
3 ; input: LSD-first ASCII digits in ymm0. Probably can't justify that and would need to byte-reverse
5 00000000 C5FD72F007 vpslld ymm0, ymm0, 7
6 00000005 C5FDD7C0 vpmovmskb eax, ymm0
7 00000009 C1E817 shr eax, 23 ; AL = exponent
8 0000000C 2C7F sub al, 127
9 0000000E C3 ret
Fun fact: AVX-512 allows vpslld ymm0, [rdi], 7
, if you had bytes in the LSD-first order in memory. (7 bytes including 4-byte EVEX, so an extra 2 bytes to take a char* instead of YMM arg.)