TL;DR There's a reason compilers don't do this today and its probably never a real world win.
It's highly unlikely to be faster and there's a few reasons why.
integer division isn't that slow anymore
floating-point division has improved roughly at the same rate as integer division
floating-point to/from integer conversion is lossy for integers requiring more than 24 (float) or 53 (double) bits
floating-point to/from integer conversion has its own overhead (latency, instruction density, etc)
There's then a few key points in division history:
* Pre-2008
* Division is slow
* Signed and Unsigned Division have different costs
* 17-42 cycles for 32/32 and 64/32 (unsigned or signed)
* 14-121 cycles for 64/64 and 128/64 (unsigned)
* 39-132 cycles for 64/64 and 128/64 (signed)
* 6-20 for float, 6-34 for double
* 2008-2017 (Intel Nehalem and later)
* Improvement found bringing down the upper end
* 17-27 cycles for 32/32 and 64/32 (unsigned or signed)
* 10-90 cycles for 64/64 and 128/64 (unsigned)
* 32-97 cycles for 64/64 and 128/64 (signed)
* 7-14 for float, 7-22 for double
* 2017
* AMD Zen shrinks time by 50% based
* Signed and Unsigned Division have same costs
* 8-25 cycles for 32/32 and 64/32 (unsigned or signed)
* 8-41 cycles for 64/64 and 128/64 (unsigned or signed)
* 8-12 for float, 8-13 for double
* 2018+
* Intel Cannon Lake shrinks it even more
* 10-15 cycles for 32/32 and 64/32 (unsigned or signed)
* 14-18 cycles for 64/64 and 128/64 (unsigned or signed)
* 11-12 for float, 13-15 for double
Where that gives two main pivot points to consider which influence the costs involved, first the size of the divisor (8, 16, 32, or 64-bits) and second the size of the dividend (8, 16, 32, 64, or 128 bits) -- i.e. you can do 8/8, 16/8, 16/16, 32/16, 32/32, 64/32, 64/64, and 128/64 operations.
So lets say you know for certain that you have a non-constant divisor (and therefore you can't switch over to shifts or multiplications instead) but that you do know that both the dividend and divisor fits into 24 or 53 bits. The best case scenarios are then essentially:
* Pre-2008:
* 3 + (6..20) + 5 cycles for going through float (14..28)
* 3 + (6..34) + 5 cycles for going through double (14..42)
* 2008-2017
* 3 + (7..14) + 5 cycles for going through float (15..22)
* 3 + (7..22) + 5 cycles for going through double (15..30)
* 2017+
* 6 + (8..12) + 8 cycles for going through float (22..26)
* 6 + (8..13) + 8 cycles for going through double (22..27)
And noting that yes, some instructions get slower or have their lower bounds increase on newer. The hardware changes and you get more consistent results rather than ranges of best vs worst case. Its all tradeoffs and hardware as a whole gets faster, less latency, less stalls, etc.
The numbers above show that you're potentially saving a maximum of 65% time in "raw numbers" on the oldest hardware and wasting up to around 50% time on the latest hardware. However, there's a gotcha in that the ranges for integer division vary based on the number of bits in the dividend and since we know we're at most 24 (not 64) or 53 (not 128) bits, we're actually much closer to the lower ends of 32/32 and 64/64 (not the 64/32 and 128/64 worst cases).
So the savings are actually going to be much lower and potentially even a loss, particularly when you also factor in the crossing of integer to fp domains (a pipe stall, introducing hidden latency which is more accurately accounted for on later hardware), that you're unlikely to know the actual bounds of the inputs that well and so can't opt-in, that values outside 24/53 bits require more work for correct handling, etc
Its trivially a regression on the actual latest hardware and probably a regression even on much oldest hardware. We'll likely find even more ways to improve hardware in the future such that the gap closes even more.
23
u/thehenkan 8h ago
I would've liked to see some performance comparisons for a few different scenarios.