Integerdeling omzetten naar floating-point is triviaal

Er zijn daarom gevallen waarin het interessant kan zijn om bepaalde integer div/mod-operaties te verplaatsen naar floating-point. Hoewel dit complex lijkt, is het in werkelijkheid vrij eenvoudig. De wiskunde is rechttoe rechtaan.

De claim

Voor twee integers $x$ en $y$ (signed of unsigned) die passen binnen 53 bits voor double precision of 24 bits voor single precision, waarbij beide zijn gepromoveerd naar floating-point, geldt het volgende in de standaard afrondingsmodus (round-to-nearest, ties to even):

// Floating-point berekening:
// d is dezelfde integer als de integerdeling x / y
// m is dezelfde integer als de integerrest x % y

d = trunc(x/y);     // floor werkt voor unsigned
m = -fma(d, y, -x); // fma (Fused Multiply-Add) is vereist

Opmerking: Als alleen d nodig is en deze vervolgens wordt geconverteerd naar een integer, dan is de truncate- of floor-operatie gratis inbegrepen bij de conversie van float naar integer.

In het vervolg wordt er alleen gekeken naar unsigned integers, omdat dit het moeilijkste scenario is (signed integers hebben een kleinere maximale magnitude), maar houd er rekening mee dat floating-point effectief een signed magnitude quantity opslaat.

Praktische overwegingen

  • Afrondingsmodi: Dit werkt door de afrondingsmodus in te stellen op TOWARD_ZERO en deze daarna terug te zetten, maar het wijzigen van control words is vaak erg duur.
  • Hardware-ondersteuning: Sommige hardware heeft divisie-opcodes waarmee de afrondingsmodus per operatie kan worden gekozen. AVX-512 heeft bijvoorbeeld de intrinsic mmdivroundsd met een geliste latentie van 14 cycli.
  • Conversiekosten: Het converteren van int naar float en terug kost tijd. (Let op: de meeste x64 CPU's hebben geen specifieke op voor unsigned naar float en terug, wat een implementatievraagstuk is.)
  • Precisie: De floating-point precisie $p$ bepaalt de bovengrens voor de breedte van de integer. Als het werkt voor $p$, dan werken alle kleinere breedtes triviaal. Voorbeelden zijn 32-bit integers in doubles ($p=53$) en 16-bit integers in singles ($p=24$). In deze gevallen is het mogelijk dat er "trucs" zijn om de int-naar-float conversie te omzeilen.
  • SIMD: De meest veelbelovende toepassing is in SIMD, om de overhead te amortiseren.
  • Constante delers: Bij constante delers zou de compiler dit moeten optimaliseren. Voor delers die tijdens runtime bekend zijn en een klein aantal keren worden hergebruikt, bestaan software-oplossingen zoals libdivide.

Er bestaan overigens reeds bewezen methoden voor dit proces (zie: Formally verified 32- and 64-bit integer division using double-precision floating-point arithmetic).

Wiskundige onderbouwing

De standaard afrondingsmodus is round-to-nearest (ties to even). De eerste belangrijke observatie betreft het "tie"-gedeelte. Een tie treedt op wanneer het exacte resultaat van een operatie exact in het midden ligt tussen twee floating-point getallen. Floating-point deling (in ons geval met basis-2 en dezelfde werkprecisie) heeft echter geen midpunten. Daarom treedt er nooit een tie op en is alleen het round-to-nearest gedeelte van toepassing.

Laten we een voorbeeld nemen met een floating-point formaat van 4-bit precisie, waarbij we kijken naar alle configuraties waarin het exacte resultaat kleiner is dan één, maar zo dicht mogelijk bij het afronden naar boven ligt:

GRSResultaatActie
`.1111\0xx`.1111Geen afronding
`.1111\100`-Tie case is onmogelijk
`.1111\101`1.000Afronden naar boven
`.1111\11x`1.000Afronden naar boven

Vóór de afronding berekent de hardware drie extra bits: guard bit (G), round bit (R) en sticky bit (S). Omdat de tie case onmogelijk is, hebben we alleen de G-bit nodig. In elk formaat waar het exacte resultaat $r$ bits heeft voor het fractionele deel, is voor afronding naar de volgende integer vereist dat het fractionele deel ten minste $r+1$ leidende enen heeft.

De claim is dat dit onmogelijk is met legale inputs.

Analyse van het fractionele deel

Om te bepalen wat er gebeurt met het fractionele deel, splitsen we het exacte resultaat van $x/y$ op in een integer-deel $n$ en een fractioneel deel:

$$\frac{x}{y} = n + \frac{a}{b}$$

Waarbij $b=y$, dus duidelijk is dat $\frac{a}{b} \in [0, 1)$.

Bij een binair floating-point formaat met precisie $p$ produceert de deling een integer van $d$-bits, waardoor er $r$-bits overblijven voor de rest:

  • $d = \lfloor \log_2(n) + 1 \rfloor$
  • $r = p - d$

Voor een fractioneel deel van $r$-bits kunnen we de onder- en bovengrens definiëren voor de waarde van $b$:

  • $\text{lower}(r) = 2^r$
  • $\text{upper}(r) = 2^{r+1} - 1$

We willen weten wat het maximale fractionele deel is dat mogelijk is voor $r$-bits. De waarde die met de ondergrens het dichtst bij 1 komt is:

$$\frac{2^r-1}{2^r} = \sum_{i=1}^r \frac{1}{2^i}$$

Deze som laat zien dat dit het kleinste getal is dat $r$ leidende enen produceert in de binaire expansie. Als we nu kijken naar de waarde van de bovengrens die het dichtst bij 1 ligt, en deze vergelijken met het kleinste getal met $r+1$ leidende enen (de ondergrens van $r+1$), zien we:

$$\frac{2^{r+1}-2}{2^{r+1}-1} < \frac{2^{r+1}-1}{2^{r+1}-0}$$

Deze ongelijkheid betekent dat de bovengrens beperkt is tot $r$ leidende enen. We kunnen specifiek zijn over de binaire expansie van de grenzen: de ondergrens bestaat uit $r$ enen en stopt daar; de exacte waarde van de bovengrens is puur cyclisch met een periode van $r+1$, bestaande uit $r$ enen gevolgd door één nul.

Overzicht voor de eerste vier waarden van $r$:

$r$OndergrensBinaire expansieBovengrensBinaire expansie
1$\frac{1}{2}$.1$\frac{2}{3}$.(10)
2$\frac{3}{4}$.11$\frac{6}{7}$.(110)
3$\frac{7}{8}$.111$\frac{14}{15}$.(1110)
4$\frac{15}{16}$.1111$\frac{30}{31}$.(11110)

Het belangrijkste punt is: voor legale inputs kan $\frac{x}{y}$ geen fractioneel deel produceren dat groot genoeg is om afronding naar de volgende integer te veroorzaken. Hierdoor is het integer-deel van het floating-point resultaat identiek aan het integer-resultaat.

Verificatie met delers

Om dit samen te vatten, kijken we naar enkele specifieke delers:

  • Delen door 1: Is correct.
  • Delen door 2: Modificeert alleen de exponent; er vindt geen afronding plaats.
  • Delen door 3: Hier is $r=1$. De grootste waarde voor $x \% 3$ is 2, dus de maximale fractie is $\frac{2}{3}$. Dit kan niet worden afgerond naar de volgende integer.
  • Delen door $\{4,5,6,7\}$: Hier is $r=2$. De exacte deler maakt niet uit, omdat ze allemaal beginnen met .110x.

Dit geldt voor alle mogelijke delers.