packages feed

atrophy-0.2.0.0: src/Atrophy/LongDivision.hs

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE DerivingStrategies #-}

-- | Dividing multi-limb numbers by a single 64-bit limb.
--
-- Uses Möller & Granlund, "Improved division by invariant integers": a 128/64
-- division costs one multiplication-and-a-half and a predictable branch, given
-- a reciprocal precomputed with a single hardware division.
--
-- Limbs are little-endian: index 0 is the least significant.
module Atrophy.LongDivision
  ( Divisor2By1
  , newDivisor2By1
  , divisor2By1
  , divRem2By1
  , longDivision
  , longDivisionInPlace
  ) where

import Atrophy.Internal (NonZero (..))
import Atrophy.Internal.Prim
import Control.DeepSeq (NFData (..))
import Control.Monad.ST (ST)
import Data.Bits
import Data.Primitive.PrimArray
import Data.Word

-- | A precomputed 64-bit divisor for 128/64 division.
data Divisor2By1 = Divisor2By1
  {-# UNPACK #-} !Word64 -- normalized divisor, top bit set
  {-# UNPACK #-} !Word64 -- reciprocal: floor ((2^128 - 1) / dn) - 2^64
  {-# UNPACK #-} !Int    -- normalization shift
  deriving stock (Eq, Show)

instance NFData Divisor2By1 where
  rnf !_ = ()

{-# INLINE newDivisor2By1 #-}
newDivisor2By1 :: NonZero Word64 -> Divisor2By1
newDivisor2By1 (NonZero d) =
  let !s = countLeadingZeros d
      !dn = unsafeShiftL d s
  in case quotRem128By64 (complement dn) maxBound dn of
    (# v, _ #) -> Divisor2By1 dn v s

-- | The original divisor.
{-# INLINE divisor2By1 #-}
divisor2By1 :: Divisor2By1 -> Word64
divisor2By1 (Divisor2By1 dn _ s) = unsafeShiftR dn s

-- | Möller-Granlund algorithm 4, on a normalized divisor. Requires @u1 < dn@.
{-# INLINE udivrem2By1 #-}
udivrem2By1 :: Word64 -> Word64 -> Word64 -> Word64 -> (# Word64, Word64 #)
udivrem2By1 u1 u0 dn v =
  case mulFull64 v u1 of { (# p1, p0 #) ->
  case addCarry64 p0 u0 of { (# q0, c #) ->
  let !q1 = p1 + u1 + c + 1
      !r = u0 - q1 * dn
      -- if r > q0 then (q1 - 1, r + dn), branchless since it is unpredictable
      !mask = negate (ltW q0 r)
      !q1' = q1 + mask
      !r' = r + (mask .&. dn)
  in if r' >= dn
    then (# q1' + 1, r' - dn #)
    else (# q1', r' #) }}

-- | @divRem2By1 d hi lo@ divides @hi * 2^64 + lo@ by @d@. Requires @hi < d@.
{-# INLINE divRem2By1 #-}
divRem2By1 :: Divisor2By1 -> Word64 -> Word64 -> (Word64, Word64)
divRem2By1 (Divisor2By1 dn v s) hi lo =
  case shl128Small hi lo s of
    (# u1, u0 #) -> case udivrem2By1 u1 u0 dn v of
      (# q, r #) -> (q, unsafeShiftR r s)

-- | Divide the little-endian number in the first argument, writing the
-- quotient limbs to the mutable array, which must be at least as long.
-- Returns the remainder.
longDivision :: Divisor2By1 -> PrimArray Word64 -> MutablePrimArray s Word64 -> ST s Word64
longDivision d numerator quotient = do
  let !len = sizeofPrimArray numerator
  qlen <- getSizeofMutablePrimArray quotient
  if qlen < len
    then error "Atrophy.LongDivision.longDivision: quotient array is too small"
    else do
      copyPrimArray quotient 0 numerator 0 len
      divInPlace d quotient len

-- | Divide the little-endian number in place, replacing it with the quotient.
-- Returns the remainder.
longDivisionInPlace :: Divisor2By1 -> MutablePrimArray s Word64 -> ST s Word64
longDivisionInPlace d arr = getSizeofMutablePrimArray arr >>= divInPlace d arr

{-# INLINE divInPlace #-}
divInPlace :: Divisor2By1 -> MutablePrimArray s Word64 -> Int -> ST s Word64
divInPlace (Divisor2By1 dn v s) arr len
  | len == 0 = pure 0
  | otherwise = do
      -- Normalize in a separate pass rather than on the fly: two tight loops
      -- beat one loop juggling shifts, since GHC's register allocator spills.
      r0 <- if s == 0 then pure 0 else shiftLeftInPlace arr len s
      let go !i !r
            | i < 0 = pure (unsafeShiftR r s)
            | otherwise = do
                u0 <- readPrimArray arr i
                case udivrem2By1 r u0 dn v of
                  (# q, r' #) -> do
                    writePrimArray arr i q
                    go (i - 1) r'
      go (len - 1) r0

-- | Shift the little-endian number left by @0 < s < 64@, returning the bits
-- shifted out of the top.
shiftLeftInPlace :: MutablePrimArray s Word64 -> Int -> Int -> ST s Word64
shiftLeftInPlace arr len s = do
  top <- readPrimArray arr (len - 1)
  let !rs = 64 - s
      go !i !cur
        | i == 0 = writePrimArray arr 0 (unsafeShiftL cur s)
        | otherwise = do
            next <- readPrimArray arr (i - 1)
            writePrimArray arr i (unsafeShiftL cur s .|. unsafeShiftR next rs)
            go (i - 1) next
  go (len - 1) top
  pure (unsafeShiftR top rs)