dataframe-core-2.5.0.0: src-internal/DataFrame/Internal/Column/Operations.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{- |
Bulk operations over columns: mapping, folding, slicing, gathering, zipping,
appending, and the mutable-column IO helpers.
-}
module DataFrame.Internal.Column.Operations where
import qualified Data.Text as T
import qualified Data.Vector as VB
import qualified Data.Vector.Generic as VG
import qualified Data.Vector.Mutable as VBM
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Control.Monad (when)
import Control.Monad.ST (runST)
import Data.Bits (setBit, shiftL, shiftR)
import Data.Int (Int32)
import Data.Kind (Type)
import Data.Maybe (catMaybes, fromMaybe, isNothing)
import Data.Type.Equality (TestEquality (..))
import Data.Word (Word8)
import DataFrame.Errors (
DataFrameException (EmptyDataSetException, TypeMismatchException),
TypeErrorContext (
MkTypeErrorContext,
callingFunctionName,
errorColumnName,
expectedType,
userType
),
)
import DataFrame.Internal.Column.Base
import DataFrame.Internal.Column.Bitmap
import DataFrame.Internal.Column.Conversion
import DataFrame.Internal.Column.Properties
import DataFrame.Internal.Column.Types
import DataFrame.Internal.Control.Concurrent (
parThreshold,
parallelChunks_,
shouldParallelize,
)
import DataFrame.Internal.Data.PackedText (
PackedOffsets,
PackedSel (..),
PackedTextData (..),
offCount,
packedGather,
packedLength,
)
import System.IO.Unsafe (unsafePerformIO)
import Type.Reflection (
TypeRep,
Typeable,
typeOf,
typeRep,
withTypeable,
type (:~:) (Refl),
)
{- | Force evaluation of all elements in a column. Replacement for the removed
@instance NFData Column@; used by the IO and lazy-executor strict paths.
-}
forceColumn :: Column -> ()
forceColumn (BoxedColumn Nothing (v :: VB.Vector a)) = VB.foldl' (const (`seq` ())) () v
forceColumn (BoxedColumn (Just bm) (v :: VB.Vector a)) =
let n = VB.length v
go !i
| i >= n = ()
| bitmapTestBit bm i = VB.unsafeIndex v i `seq` go (i + 1)
| otherwise = go (i + 1)
in go 0
forceColumn (UnboxedColumn _ v) = v `seq` ()
forceColumn (PackedText _ (PackedTextData arr offs sel _)) = arr `seq` offs `seq` sel `seq` ()
forceColumn (MergedColumn a b) =
forceColumn a `seq` forceColumn b `seq` checkMergedNoBothNull a b
{- | 'MergedColumn' defers element construction, so forcing must still surface
the one deferred error — a row null on both sides — inside strict IO/executor
boundaries. O(rows) bitmap walk, no allocation; both-null needs a bitmap on
each side, so anything else passes immediately.
-}
checkMergedNoBothNull :: Column -> Column -> ()
checkMergedNoBothNull a b = case (columnBitmap a, columnBitmap b) of
(Just ba, Just bb) ->
let !n = min (columnLength a) (columnLength b)
go !i
| i >= n = ()
| bitmapTestBit ba i || bitmapTestBit bb i = go (i + 1)
| otherwise = error "mkMergedColumns: both null"
in go 0
_ -> ()
-- | Allocate a mutable column of size @n@ matching the constructor/type of the given column.
newMutableColumn :: Int -> Column -> IO MutableColumn
newMutableColumn n (BoxedColumn _ (_ :: VB.Vector a)) =
MBoxedColumn <$> (VBM.new n :: IO (VBM.IOVector a))
newMutableColumn n (UnboxedColumn _ (_ :: VU.Vector a)) =
MUnboxedColumn <$> (VUM.new n :: IO (VUM.IOVector a))
newMutableColumn n c@(PackedText _ _) = newMutableColumn n (materializePacked c)
newMutableColumn n c@(MergedColumn _ _) = newMutableColumn n (materializeMerged c)
-- | Copy a column chunk into a mutable column starting at offset @off@.
copyIntoMutableColumn :: MutableColumn -> Int -> Column -> IO ()
copyIntoMutableColumn mv off c@(MergedColumn _ _) =
copyIntoMutableColumn mv off (materializeMerged c)
copyIntoMutableColumn (MBoxedColumn (mv :: VBM.IOVector b)) off (BoxedColumn _ (v :: VB.Vector a)) =
case testEquality (typeRep @a) (typeRep @b) of
Just Refl -> VG.imapM_ (\i x -> VBM.unsafeWrite mv (off + i) x) v
Nothing -> error "copyIntoMutableColumn: Boxed type mismatch"
copyIntoMutableColumn (MUnboxedColumn (mv :: VUM.IOVector b)) off (UnboxedColumn _ (v :: VU.Vector a)) =
case testEquality (typeRep @a) (typeRep @b) of
Just Refl -> VG.imapM_ (\i x -> VUM.unsafeWrite mv (off + i) x) v
Nothing -> error "copyIntoMutableColumn: Unboxed type mismatch"
copyIntoMutableColumn mc off c@(PackedText _ _) =
copyIntoMutableColumn mc off (materializePacked c)
copyIntoMutableColumn _ _ _ =
error "copyIntoMutableColumn: constructor mismatch"
-- | Freeze a mutable column into an immutable column.
freezeMutableColumn :: MutableColumn -> IO Column
freezeMutableColumn (MBoxedColumn mv) = BoxedColumn Nothing <$> VB.unsafeFreeze mv
freezeMutableColumn (MUnboxedColumn mv) = UnboxedColumn Nothing <$> VU.unsafeFreeze mv
-- | An internal function to map a function over the values of a column.
mapColumn ::
forall b c.
(Columnable b, Columnable c) =>
(b -> c) -> Column -> Either DataFrameException Column
mapColumn f = \case
BoxedColumn bm (col :: VB.Vector a) -> runBoxed bm col
UnboxedColumn bm (col :: VU.Vector a) -> runUnboxed bm col
c@(PackedText _ _) -> mapColumn f (materializePacked c)
c@(MergedColumn _ _) -> mapColumn f (materializeMerged c)
where
runBoxed ::
forall a.
(Columnable a) =>
Maybe Bitmap -> VB.Vector a -> Either DataFrameException Column
runBoxed bm col = case testEquality (typeRep @b) (typeRep @(Maybe a)) of
Just Refl ->
let !n = VB.length col
in Right $ case sUnbox @c of
STrue -> UnboxedColumn Nothing $
parGenerateUnboxed n $ \i ->
f
( if maybe True (`bitmapTestBit` i) bm
then Just (VB.unsafeIndex col i)
else Nothing
)
SFalse -> fromVector @c $
VB.generate n $ \i ->
f
( if maybe True (`bitmapTestBit` i) bm
then Just (VB.unsafeIndex col i)
else Nothing
)
Nothing -> case testEquality (typeRep @a) (typeRep @b) of
Just Refl ->
Right $ case sUnbox @c of
STrue ->
UnboxedColumn
bm
(parGenerateUnboxed (VB.length col) (f . VB.unsafeIndex col))
SFalse -> case bm of
Nothing -> fromVector @c (VB.map f col)
Just _ -> BoxedColumn bm (VB.map f col)
Nothing -> throwTypeMismatch @a @b
runUnboxed ::
forall a.
(Columnable a, VU.Unbox a) =>
Maybe Bitmap -> VU.Vector a -> Either DataFrameException Column
runUnboxed bm col = case testEquality (typeRep @b) (typeRep @(Maybe a)) of
Just Refl ->
let !n = VU.length col
in Right $ case sUnbox @c of
STrue -> UnboxedColumn Nothing $
parGenerateUnboxed n $ \i ->
f
( if maybe True (`bitmapTestBit` i) bm
then Just (VU.unsafeIndex col i)
else Nothing
)
SFalse -> fromVector @c $
VB.generate n $ \i ->
f
( if maybe True (`bitmapTestBit` i) bm
then Just (VU.unsafeIndex col i)
else Nothing
)
Nothing -> case testEquality (typeRep @a) (typeRep @b) of
Just Refl -> Right $ case sUnbox @c of
STrue ->
UnboxedColumn
bm
(parGenerateUnboxed (VU.length col) (f . VU.unsafeIndex col))
SFalse -> case bm of
Nothing -> fromVector @c (VB.generate (VU.length col) (f . VU.unsafeIndex col))
Just _ -> BoxedColumn bm (VB.generate (VU.length col) (f . VU.unsafeIndex col))
Nothing -> throwTypeMismatch @a @b
{-# INLINEABLE mapColumn #-}
-- | Applies a function that returns an unboxed result to an unboxed vector, storing the result in a column.
imapColumn ::
forall b c.
(Columnable b, Columnable c) =>
(Int -> b -> c) -> Column -> Either DataFrameException Column
imapColumn f = \case
BoxedColumn bm (col :: VB.Vector a) -> runBoxed bm col
UnboxedColumn bm (col :: VU.Vector a) -> runUnboxed bm col
c@(PackedText _ _) -> imapColumn f (materializePacked c)
c@(MergedColumn _ _) -> imapColumn f (materializeMerged c)
where
runBoxed ::
forall a.
(Columnable a) =>
Maybe Bitmap -> VB.Vector a -> Either DataFrameException Column
runBoxed bm col = case testEquality (typeRep @a) (typeRep @b) of
Just Refl -> Right $ case sUnbox @c of
STrue ->
UnboxedColumn
bm
(VU.generate (VB.length col) (\i -> f i (VB.unsafeIndex col i)))
SFalse -> BoxedColumn bm (VB.imap f col)
Nothing -> throwTypeMismatch @a @b
runUnboxed ::
forall a.
(Columnable a, VU.Unbox a) =>
Maybe Bitmap -> VU.Vector a -> Either DataFrameException Column
runUnboxed bm col = case testEquality (typeRep @a) (typeRep @b) of
Just Refl -> Right $ case sUnbox @c of
STrue -> UnboxedColumn bm (VU.imap f col)
SFalse -> BoxedColumn bm (VB.imap f (VG.convert col))
Nothing -> throwTypeMismatch @a @b
-- | O(n) Takes the last n values of a column.
takeLastColumn :: Int -> Column -> Column
takeLastColumn n column = sliceColumn (columnLength column - n) n column
{-# INLINE takeLastColumn #-}
-- | O(n) Takes n values after a given column index.
sliceColumn :: Int -> Int -> Column -> Column
sliceColumn start n (MergedColumn a b) =
MergedColumn (sliceColumn start n a) (sliceColumn start n b)
sliceColumn start n (BoxedColumn bm xs) =
BoxedColumn (fmap (bitmapSlice start n) bm) (VG.slice start n xs)
sliceColumn start n (UnboxedColumn bm xs) =
UnboxedColumn (fmap (bitmapSlice start n) bm) (VG.slice start n xs)
sliceColumn start n (PackedText bm p)
-- packedGather decodes an out-of-range index as the empty string, where
-- the boxed and unboxed arms reject the slice, so check first.
| start < 0 || n < 0 || start + n > packedLength p =
errorWithoutStackTrace
( "sliceColumn: invalid slice ("
++ show start
++ ","
++ show n
++ ","
++ show (packedLength p)
++ ")"
)
| otherwise =
PackedText
(fmap (bitmapSlice start n) bm)
(packedGather (VU.enumFromN start n) p)
{-# INLINE sliceColumn #-}
-- | O(n) Selects the elements at a given set of indices. Does not change the order.
-------------------------------------------------------------------------------
-- Parallel element-wise kernels
-------------------------------------------------------------------------------
{- | Parallel unboxed 'VU.generate': splits the index space into one contiguous
chunk per capability, evaluates each chunk into its disjoint slice of a single
pre-allocated mutable vector, then freezes. Element @i@ depends only on @f i@,
so the result is bit-identical to the sequential 'VU.generate' regardless of
capability count. Falls back to 'VU.generate' below 'parThreshold'.
-}
{-# SPECIALIZE parGenerateUnboxed ::
Int -> (Int -> Double) -> VU.Vector Double
#-}
{-# SPECIALIZE parGenerateUnboxed ::
Int -> (Int -> Float) -> VU.Vector Float
#-}
{-# SPECIALIZE parGenerateUnboxed :: Int -> (Int -> Int) -> VU.Vector Int #-}
{-# SPECIALIZE parGenerateUnboxed :: Int -> (Int -> Bool) -> VU.Vector Bool #-}
parGenerateUnboxed :: (VU.Unbox c) => Int -> (Int -> c) -> VU.Vector c
parGenerateUnboxed n f
| not (shouldParallelize parThreshold n) = VU.generate n f
| otherwise = unsafePerformIO $ do
mv <- VUM.unsafeNew n
parallelChunks_ parThreshold n (fillGenerate mv f)
VU.unsafeFreeze mv
{-# NOINLINE parGenerateUnboxed #-}
{- | The chunk body shared by 'parGenerateUnboxed' and
'parGenerateUnboxedInline'. INLINE so the latter's monomorphic wrappers each
get their own copy with @f@ inlined.
-}
fillGenerate ::
(VU.Unbox c) => VUM.IOVector c -> (Int -> c) -> Int -> Int -> IO ()
fillGenerate mv f !lo !hi =
let go !i
| i >= hi = pure ()
| otherwise = VUM.unsafeWrite mv i (f i) >> go (i + 1)
in go lo
{-# INLINE fillGenerate #-}
{- | Parallel unboxed gather: element @i@ of the result is
@v ! (ix ! i)@ (unsafe indexing — callers pass in-bounds index vectors, e.g.
grouping-produced representative rows). Same chunking as 'parGenerateUnboxed'
(one contiguous chunk per capability into disjoint slices of one buffer), so
the result is bit-identical to the sequential backpermute at any capability
count; falls back to a sequential loop below 'parThreshold'.
-}
parBackpermuteUnboxed ::
(VU.Unbox a) => VU.Vector a -> VU.Vector Int -> VU.Vector a
parBackpermuteUnboxed v ix =
parGenerateUnboxed (VU.length ix) (VU.unsafeIndex v . VU.unsafeIndex ix)
{-# INLINE parBackpermuteUnboxed #-}
{- | 'parGenerateUnboxed' with an INLINE body: each monomorphic NOINLINE
wrapper below gets its own copy of the fill loop with @f@ inlined, so the
per-element unknown closure call (and the boxed result it returns) disappears
— on a 1e8-row gather that call+alloc dominated the whole pass. Same chunking,
bit-identical results; wrappers must stay NOINLINE so the 'unsafePerformIO'
runs once per call.
-}
parGenerateUnboxedInline :: (VU.Unbox c) => Int -> (Int -> c) -> VU.Vector c
parGenerateUnboxedInline n f
| not (shouldParallelize parThreshold n) = VU.generate n f
| otherwise = unsafePerformIO $ do
mv <- VUM.unsafeNew n
parallelChunks_ parThreshold n (fillGenerate mv f)
VU.unsafeFreeze mv
{-# INLINE parGenerateUnboxedInline #-}
-- | Closure-free parallel 'Int' gather: @out!i = v ! (ix!i)@.
parBackpermuteInt :: VU.Vector Int -> VU.Vector Int -> VU.Vector Int
parBackpermuteInt v ix =
parGenerateUnboxedInline
(VU.length ix)
(VU.unsafeIndex v . VU.unsafeIndex ix)
{-# NOINLINE parBackpermuteInt #-}
-- | Closure-free parallel 'Double' gather: @out!i = v ! (ix!i)@.
parBackpermuteDouble :: VU.Vector Double -> VU.Vector Int -> VU.Vector Double
parBackpermuteDouble v ix =
parGenerateUnboxedInline
(VU.length ix)
(VU.unsafeIndex v . VU.unsafeIndex ix)
{-# NOINLINE parBackpermuteDouble #-}
{- | Closure-free double-indirection gather: @out!g = vis ! (offs!g)@ over
@length offs - 1@ groups (the representative-row build of the 'Grouped'
pattern).
-}
parBackpermute2Int :: VU.Vector Int -> VU.Vector Int -> VU.Vector Int
parBackpermute2Int vis offs =
parGenerateUnboxedInline
(max 0 (VU.length offs - 1))
(VU.unsafeIndex vis . VU.unsafeIndex offs)
{-# NOINLINE parBackpermute2Int #-}
{- | Parallel boxed gather. The read side uses 'VB.unsafeIndexM' so the array
slot is fetched eagerly (element pointers are shared, elements themselves stay
un-forced, exactly as the sequential 'VB.generate' gather). Bit-identical
element values; sequential below 'parThreshold'.
-}
parBackpermuteBoxed :: VB.Vector a -> VU.Vector Int -> VB.Vector a
parBackpermuteBoxed v ix
| not (shouldParallelize parThreshold n) =
VB.generate n ((v `VB.unsafeIndex`) . (ix `VU.unsafeIndex`))
| otherwise = unsafePerformIO $ do
mv <- VBM.unsafeNew n
parallelChunks_ parThreshold n $ \ !lo !hi ->
let go !i
| i >= hi = pure ()
| otherwise = do
x <- VB.unsafeIndexM v (VU.unsafeIndex ix i)
VBM.unsafeWrite mv i x
go (i + 1)
in go lo
VB.unsafeFreeze mv
where
!n = VU.length ix
{-# NOINLINE parBackpermuteBoxed #-}
{- | Clamp sentinel (negative) indices to 0 in one closure-free parallel pass.
The clamped rows read row 0's value; callers mask them via the sentinel bitmap.
-}
parClampNonNeg :: VU.Vector Int -> VU.Vector Int
parClampNonNeg ix =
parGenerateUnboxedInline
(VU.length ix)
(\i -> let !x = VU.unsafeIndex ix i in max x 0)
{-# NOINLINE parClampNonNeg #-}
{- | Validity bitmap from sentinel indices (bit @i@ valid iff @ix!i >= 0@),
built one byte (8 rows) per element in parallel.
-}
parBitmapNonNeg :: VU.Vector Int -> Bitmap
parBitmapNonNeg ix =
let !n = VU.length ix
!nBytes = (n + 7) `shiftR` 3
in parGenerateUnboxedInline nBytes $ \b ->
let !base = b `shiftL` 3
go !acc !bit
| bit >= 8 = acc
| otherwise =
let !idx = base + bit
!acc' =
if idx < n && VU.unsafeIndex ix idx >= 0
then setBit acc bit
else acc
in go acc' (bit + 1)
in go (0 :: Word8) 0
{-# NOINLINE parBitmapNonNeg #-}
atIndicesStable :: VU.Vector Int -> Column -> Column
atIndicesStable indexes (BoxedColumn bm column) =
BoxedColumn
( fmap
( \bm0 ->
buildBitmapFromValid $
VU.map (\i -> if bitmapTestBit bm0 i then 1 else 0) indexes
)
bm
)
(parBackpermuteBoxed column indexes)
atIndicesStable indexes (UnboxedColumn bm (column :: VU.Vector a)) =
UnboxedColumn
( fmap
( \bm0 ->
buildBitmapFromValid $
VU.map (\i -> if bitmapTestBit bm0 i then 1 else 0) indexes
)
bm
)
-- Int/Double hit the closure-free monomorphic kernels; anything else
-- keeps the generic (per-element closure call) path.
( case testEquality (typeRep @a) (typeRep @Int) of
Just Refl -> parBackpermuteInt column indexes
Nothing -> case testEquality (typeRep @a) (typeRep @Double) of
Just Refl -> parBackpermuteDouble column indexes
Nothing -> parBackpermuteUnboxed column indexes
)
atIndicesStable indexes (MergedColumn a b) =
MergedColumn (atIndicesStable indexes a) (atIndicesStable indexes b)
atIndicesStable indexes (PackedText bm p) =
PackedText
( fmap
( \bm0 ->
buildBitmapFromValid $
VU.map (\i -> if bitmapTestBit bm0 i then 1 else 0) indexes
)
bm
)
(packedGather indexes p)
{-# INLINE atIndicesStable #-}
{- | Like 'atIndicesStable' but treats negative indices as null.
Keeps the index vector fully unboxed (no @VB.Vector (Maybe Int)@).
-}
gatherWithSentinel :: VU.Vector Int -> Column -> Column
gatherWithSentinel indices c@(MergedColumn _ _) =
gatherWithSentinel indices (materializeMerged c)
gatherWithSentinel indices col =
let !n = VU.length indices
!newBm = parBitmapNonNeg indices
withBm srcBm = case srcBm of
Nothing -> Just newBm
Just sb -> Just (andBitmaps newBm (gatherSrcBm sb))
-- Sequential fallback: gather an existing source bitmap through the
-- raw indices (negative-guarded). Only runs when the source side is
-- itself nullable, which join build sides normally are not.
gatherSrcBm sb =
buildBitmapFromValid $ VU.generate n $ \i ->
let idx = VU.unsafeIndex indices i
in if idx >= 0 && bitmapTestBit sb idx then 1 else 0
in case col of
-- packedGather composes -1 sentinels into the selector natively
-- (and shares the byte buffers), so text takes the raw indices.
PackedText srcBm p -> PackedText (withBm srcBm) (packedGather indices p)
BoxedColumn srcBm v
-- An empty source means every index is a sentinel; clamping
-- would read row 0 of an empty vector.
| VB.null v -> BoxedColumn (withBm srcBm) (allNullBoxed n v)
| otherwise ->
BoxedColumn
(withBm srcBm)
(parBackpermuteBoxed v (parClampNonNeg indices))
UnboxedColumn srcBm v
| VU.null v -> UnboxedColumn (withBm srcBm) (allNullUnboxed n v)
| otherwise ->
-- Reuse atIndicesStable's Int/Double monomorphic kernel
-- dispatch for the payload; the bitmap is replaced below.
case atIndicesStable (parClampNonNeg indices) (UnboxedColumn Nothing v) of
UnboxedColumn _ dat -> UnboxedColumn (withBm srcBm) dat
other -> other
{-# INLINE gatherWithSentinel #-}
{- | An @n@-row payload for a gather whose source is empty: every index is a
sentinel, so the sentinel bitmap masks every row and no element is ever read.
The source vector is passed only to fix the element type.
-}
allNullBoxed :: Int -> VB.Vector a -> VB.Vector a
allNullBoxed n _ = VB.replicate n (error "gatherWithSentinel: null row forced")
-- | 'allNullBoxed' for unboxed payloads; the buffer is left uninitialised.
allNullUnboxed :: (VU.Unbox a) => Int -> VU.Vector a -> VU.Vector a
allNullUnboxed n _ = runST (VUM.new n >>= VU.unsafeFreeze)
-- | Internal helper to get indices in a boxed vector.
getIndices :: VU.Vector Int -> VB.Vector a -> VB.Vector a
getIndices indices xs = VB.generate (VU.length indices) (\i -> xs VB.! (indices VU.! i))
{-# INLINE getIndices #-}
-- | Internal helper to get indices in an unboxed vector.
getIndicesUnboxed :: (VU.Unbox a) => VU.Vector Int -> VU.Vector a -> VU.Vector a
getIndicesUnboxed indices xs = VU.generate (VU.length indices) (\i -> xs VU.! (indices VU.! i))
{-# INLINE getIndicesUnboxed #-}
findIndices ::
forall a.
(Columnable a) =>
(a -> Bool) ->
Column ->
Either DataFrameException (VU.Vector Int)
findIndices predicate = \case
BoxedColumn _ (v :: VB.Vector b) -> run v VG.convert
UnboxedColumn _ (v :: VU.Vector b) -> run v id
c@(PackedText _ _) -> findIndices predicate (materializePacked c)
c@(MergedColumn _ _) -> findIndices predicate (materializeMerged c)
where
run ::
forall b v.
(Typeable b, VG.Vector v b, VG.Vector v Int) =>
v b ->
(v Int -> VU.Vector Int) ->
Either DataFrameException (VU.Vector Int)
run column finalize = case testEquality (typeRep @a) (typeRep @b) of
Just Refl -> Right . finalize $ VG.findIndices predicate column
Nothing ->
Left $
TypeMismatchException
MkTypeErrorContext
{ userType = Right (typeRep @a)
, expectedType = Right (typeRep @b)
, callingFunctionName = Just "findIndices"
, errorColumnName = Nothing
}
-- | Fold (right) column with index.
ifoldrColumn ::
forall a b.
(Columnable a, Columnable b) =>
(Int -> a -> b -> b) -> b -> Column -> Either DataFrameException b
ifoldrColumn f acc = \case
BoxedColumn _ column -> foldrWorker column
UnboxedColumn _ column -> foldrWorker column
c@(PackedText _ _) -> ifoldrColumn f acc (materializePacked c)
c@(MergedColumn _ _) -> ifoldrColumn f acc (materializeMerged c)
where
foldrWorker ::
forall c v.
(Typeable c, VG.Vector v c) =>
v c ->
Either DataFrameException b
foldrWorker vec = case testEquality (typeRep @a) (typeRep @c) of
Just Refl -> pure $ VG.ifoldr f acc vec
Nothing ->
Left $
TypeMismatchException
( MkTypeErrorContext
{ userType = Right (typeRep @a)
, expectedType = Right (typeRep @c)
, callingFunctionName = Just "ifoldrColumn"
, errorColumnName = Nothing
}
)
foldlColumn ::
forall a b.
(Columnable a, Columnable b) =>
(b -> a -> b) -> b -> Column -> Either DataFrameException b
foldlColumn f acc = \case
BoxedColumn _ column -> foldlWorker column
UnboxedColumn _ column -> foldlWorker column
c@(PackedText _ _) -> foldlColumn f acc (materializePacked c)
c@(MergedColumn _ _) -> foldlColumn f acc (materializeMerged c)
where
foldlWorker ::
forall c v.
(Typeable c, VG.Vector v c) =>
v c ->
Either DataFrameException b
foldlWorker vec = case testEquality (typeRep @a) (typeRep @c) of
Just Refl -> pure $ VG.foldl' f acc vec
Nothing ->
Left $
TypeMismatchException
( MkTypeErrorContext
{ userType = Right (typeRep @a)
, expectedType = Right (typeRep @c)
, callingFunctionName = Just "ifoldrColumn"
, errorColumnName = Nothing
}
)
foldl1Column ::
forall a.
(Columnable a) =>
(a -> a -> a) -> Column -> Either DataFrameException a
foldl1Column f = \case
BoxedColumn _ column -> foldl1Worker column
UnboxedColumn _ column -> foldl1Worker column
c@(PackedText _ _) -> foldl1Column f (materializePacked c)
c@(MergedColumn _ _) -> foldl1Column f (materializeMerged c)
where
foldl1Worker ::
forall c v.
(Typeable c, VG.Vector v c) =>
v c ->
Either DataFrameException a
foldl1Worker vec = case testEquality (typeRep @a) (typeRep @c) of
Just Refl -> pure $ VG.foldl1' f vec
Nothing ->
Left $
TypeMismatchException
( MkTypeErrorContext
{ userType = Right (typeRep @a)
, expectedType = Right (typeRep @c)
, callingFunctionName = Just "foldl1Column"
, errorColumnName = Nothing
}
)
{- | O(n) Seedless fold over groups using the first element of each group as seed.
Like 'foldDirectGroups' but for the case where no initial accumulator is available.
-}
foldl1DirectGroups ::
forall a.
(Columnable a) =>
(a -> a -> a) ->
Column ->
VU.Vector Int ->
VU.Vector Int ->
Either DataFrameException Column
foldl1DirectGroups f col valueIndices offsets
| VU.length offsets <= 1 = pure $ fromVector @a VB.empty
| otherwise = case col of
UnboxedColumn _ (vec :: VU.Vector d) -> UnboxedColumn Nothing <$> foldl1Worker vec
BoxedColumn _ (vec :: VB.Vector d) -> BoxedColumn Nothing <$> foldl1Worker vec
PackedText _ _ -> foldl1DirectGroups f (materializePacked col) valueIndices offsets
MergedColumn _ _ -> foldl1DirectGroups f (materializeMerged col) valueIndices offsets
where
foldl1Worker ::
forall c v.
(Typeable c, VG.Vector v c) =>
v c ->
Either DataFrameException (v c)
foldl1Worker vec = case testEquality (typeRep @a) (typeRep @c) of
Just Refl ->
Right $
VG.generate (VU.length offsets - 1) foldGroup
where
foldGroup k =
let !s = VU.unsafeIndex offsets k
!e = VU.unsafeIndex offsets (k + 1)
!seed = VG.unsafeIndex vec (VU.unsafeIndex valueIndices s)
in go (s + 1) e seed
go !i !e !acc
| i >= e = acc
| otherwise =
go (i + 1) e $!
f acc (VG.unsafeIndex vec (VU.unsafeIndex valueIndices i))
Nothing ->
Left $
TypeMismatchException
MkTypeErrorContext
{ userType = Right (typeRep @a)
, expectedType = Right (typeRep @c)
, callingFunctionName = Just "foldl1DirectGroups"
, errorColumnName = Nothing
}
{-# INLINEABLE foldl1DirectGroups #-}
{- | O(n) fold over groups by scanning the column linearly (rowToGroup[i] = group
of row i). Random writes hit the small per-group accumulator array; when @acc@ is
unboxable that array is unboxed, avoiding pointer indirection.
-}
foldLinearGroups ::
forall b acc.
(Columnable b, Columnable acc) =>
(acc -> b -> acc) ->
acc ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
foldLinearGroups f seed col rowToGroup nGroups
| nGroups == 0 = Right (fromVector @acc VB.empty)
| otherwise = case col of
UnboxedColumn _ (vec :: VU.Vector d) -> foldLinearWorker vec
BoxedColumn _ (vec :: VB.Vector d) -> foldLinearWorker vec
PackedText _ _ ->
foldLinearGroups f seed (materializePacked col) rowToGroup nGroups
MergedColumn _ _ ->
foldLinearGroups f seed (materializeMerged col) rowToGroup nGroups
where
foldLinearWorker ::
forall c v.
(Typeable c, VG.Vector v c) =>
v c ->
Either DataFrameException Column
foldLinearWorker vec = case testEquality (typeRep @b) (typeRep @c) of
Just Refl ->
Right $
unsafePerformIO $
runWith
( \readAt writeAt ->
VG.iforM_ vec $ \row x -> do
let !k = VG.unsafeIndex rowToGroup row
cur <- readAt k
writeAt k $! f cur x
)
Nothing ->
Left $
TypeMismatchException
MkTypeErrorContext
{ userType = Right (typeRep @b)
, expectedType = Right (typeRep @c)
, callingFunctionName = Just "foldLinearGroups"
, errorColumnName = Nothing
}
runWith :: ((Int -> IO acc) -> (Int -> acc -> IO ()) -> IO ()) -> IO Column
runWith body = case sUnbox @acc of
STrue -> do
accs <- VUM.replicate nGroups seed
body (VUM.unsafeRead accs) (VUM.unsafeWrite accs)
UnboxedColumn Nothing <$> VU.unsafeFreeze accs
SFalse -> do
accs <- VBM.replicate nGroups seed
body (VBM.unsafeRead accs) (VBM.unsafeWrite accs)
fromVector @acc <$> VB.unsafeFreeze accs
{-# INLINE runWith #-}
{-# INLINEABLE foldLinearGroups #-}
headColumn :: forall a. (Columnable a) => Column -> Either DataFrameException a
headColumn = \case
BoxedColumn _ col -> headWorker col
UnboxedColumn _ col -> headWorker col
c@(PackedText _ _) -> headColumn (materializePacked c)
c@(MergedColumn _ _) -> headColumn (mergedHead c)
where
headWorker ::
forall c v.
(Typeable c, VG.Vector v c) =>
v c ->
Either DataFrameException a
headWorker vec = case testEquality (typeRep @a) (typeRep @c) of
Just Refl ->
if VG.null vec
then Left (EmptyDataSetException "headColumn")
else pure (VG.head vec)
Nothing ->
Left $
TypeMismatchException
( MkTypeErrorContext
{ userType = Right (typeRep @a)
, expectedType = Right (typeRep @c)
, callingFunctionName = Just "headColumn"
, errorColumnName = Nothing
}
)
-- | An internal, column version of zip.
zipColumns :: Column -> Column -> Column
zipColumns l@(MergedColumn _ _) r = zipColumns (materializeMerged l) r
zipColumns l r@(MergedColumn _ _) = zipColumns l (materializeMerged r)
zipColumns l@(PackedText _ _) r = zipColumns (materializePacked l) r
zipColumns l r@(PackedText _ _) = zipColumns l (materializePacked r)
zipColumns (BoxedColumn _ column) (BoxedColumn _ other) = BoxedColumn Nothing (VG.zip column other)
zipColumns (BoxedColumn _ column) (UnboxedColumn _ other) =
BoxedColumn
Nothing
( VB.generate
(min (VG.length column) (VG.length other))
(\i -> (column VG.! i, other VG.! i))
)
zipColumns (UnboxedColumn _ column) (BoxedColumn _ other) =
BoxedColumn
Nothing
( VB.generate
(min (VG.length column) (VG.length other))
(\i -> (column VG.! i, other VG.! i))
)
zipColumns (UnboxedColumn _ column) (UnboxedColumn _ other) = UnboxedColumn Nothing (VG.zip column other)
{-# INLINE zipColumns #-}
-- | An internal, column version of zipWith.
zipWithColumns ::
forall a b c.
(Columnable a, Columnable b, Columnable c) =>
(a -> b -> c) -> Column -> Column -> Either DataFrameException Column
zipWithColumns f (UnboxedColumn bmL (column :: VU.Vector d)) (UnboxedColumn bmR (other :: VU.Vector e)) = case testEquality (typeRep @a) (typeRep @d) of
Just Refl -> case testEquality (typeRep @b) (typeRep @e) of
Just Refl
| isNothing bmL
, isNothing bmR ->
pure $ case sUnbox @c of
STrue ->
let !n = min (VU.length column) (VU.length other)
in UnboxedColumn Nothing $
parGenerateUnboxed n $ \i ->
f (VU.unsafeIndex column i) (VU.unsafeIndex other i)
SFalse -> fromVector $ VB.zipWith f (VG.convert column) (VG.convert other)
_ -> zipWithColumnsGeneral f (UnboxedColumn bmL column) (UnboxedColumn bmR other)
Nothing -> zipWithColumnsGeneral f (UnboxedColumn bmL column) (UnboxedColumn bmR other)
-- TODO: mchavinda - reuse pattern from interpret where we augment the
-- error at the end.
zipWithColumns f left right = zipWithColumnsGeneral f left right
zipWithColumnsGeneral ::
forall a b c.
(Columnable a, Columnable b, Columnable c) =>
(a -> b -> c) -> Column -> Column -> Either DataFrameException Column
zipWithColumnsGeneral f left right = case toVector @a left of
Left (TypeMismatchException context) ->
Left $
TypeMismatchException (context{callingFunctionName = Just "zipWithColumns"})
Left e -> Left e
Right left' -> case toVector @b right of
Left (TypeMismatchException context) ->
Left $
TypeMismatchException (context{callingFunctionName = Just "zipWithColumns"})
Left e -> Left e
Right right' -> pure $ fromVector $ VB.zipWith f left' right'
{-# INLINE zipWithColumnsGeneral #-}
{-# INLINE zipWithColumns #-}
-- writeColumn and freezeColumn' (CSV-ingest helpers) moved to
-- DataFrame.IO.Internal.MutableColumn so the core column module does not
-- need to depend on DataFrame.Internal.Parsing.
{- | Freeze a mutable column into an @Either Text a@ column: every recorded
null position becomes @Left rawText@ (preserving the original input), every
other position becomes @Right v@. Used by CSV readers under 'EitherRead' mode.
-}
freezeColumnEither :: [(Int, T.Text)] -> MutableColumn -> IO Column
freezeColumnEither nulls (MBoxedColumn col) = do
frozen <- VB.unsafeFreeze col
let nullMap = nulls
pure $
BoxedColumn Nothing $
VB.imap
( \i v -> case lookup i nullMap of
Just t -> Left t
Nothing -> Right v
)
frozen
freezeColumnEither nulls (MUnboxedColumn col) = do
c <- VU.unsafeFreeze col
let nullMap = nulls
pure $
BoxedColumn Nothing $
VB.generate (VU.length c) $ \i ->
case lookup i nullMap of
Just t -> Left t
Nothing -> Right (c VU.! i)
{-# INLINE freezeColumnEither #-}
{- | Promote a non-nullable column to a nullable one (add an all-valid bitmap).
No-op when already nullable.
-}
ensureOptional :: Column -> Column
ensureOptional c@(MergedColumn _ _) = ensureOptional (materializeMerged c)
ensureOptional c@(BoxedColumn (Just _) _) = c
ensureOptional (BoxedColumn Nothing col) =
BoxedColumn (Just (allValidBitmap (VB.length col))) col
ensureOptional c@(UnboxedColumn (Just _) _) = c
ensureOptional (UnboxedColumn Nothing col) =
UnboxedColumn (Just (allValidBitmap (VU.length col))) col
ensureOptional c@(PackedText (Just _) _) = c
ensureOptional (PackedText Nothing p) =
PackedText (Just (allValidBitmap (packedLength p))) p
-- | Fills the end of a column, up to n, with null rows. Does nothing if column has length >= n.
expandColumn :: Int -> Column -> Column
expandColumn n c@(MergedColumn a b)
| n <= min (columnLength a) (columnLength b) = c
| otherwise = expandColumn n (materializeMerged c)
expandColumn n c@(PackedText _ p)
| n <= packedLength p = c
| otherwise = expandColumn n (materializePacked c)
expandColumn n column@(BoxedColumn bm col)
| n <= VG.length col = column
| otherwise =
let extra = n - VG.length col
newBm = case bm of
Nothing -> Just (buildBitmapFromNulls' n (VU.enumFromN (VG.length col) extra))
Just b ->
Just
(bitmapConcat (VG.length col) b extra (VU.replicate ((extra + 7) `shiftR` 3) 0))
newCol = col <> VB.replicate extra (errorWithoutStackTrace "expandColumn: null slot")
in BoxedColumn newBm newCol
expandColumn n column@(UnboxedColumn bm col)
| n <= VG.length col = column
| otherwise =
let extra = n - VG.length col
newBm = case bm of
Nothing -> Just (buildBitmapFromNulls' n (VU.enumFromN (VG.length col) extra))
Just b ->
Just
(bitmapConcat (VG.length col) b extra (VU.replicate ((extra + 7) `shiftR` 3) 0))
newCol = runST $ do
mv <- VUM.new n
VU.imapM_ (VUM.unsafeWrite mv) col
VU.unsafeFreeze mv
in UnboxedColumn newBm newCol
-- | Fills the beginning of a column, up to n, with null rows. Does nothing if column has length >= n.
leftExpandColumn :: Int -> Column -> Column
leftExpandColumn n c@(MergedColumn a b)
| n <= min (columnLength a) (columnLength b) = c
| otherwise = leftExpandColumn n (materializeMerged c)
leftExpandColumn n c@(PackedText _ p)
| n <= packedLength p = c
| otherwise = leftExpandColumn n (materializePacked c)
leftExpandColumn n column@(BoxedColumn bm col)
| n <= VG.length col = column
| otherwise =
let extra = n - VG.length col
origLen = VG.length col
newBm = case bm of
Nothing -> Just (buildBitmapFromNulls' n (VU.enumFromN 0 extra))
Just b ->
let nullPart = VU.replicate ((extra + 7) `shiftR` 3) 0
in Just (bitmapConcat extra nullPart origLen b)
newCol =
VB.replicate extra (errorWithoutStackTrace "leftExpandColumn: null slot") <> col
in BoxedColumn newBm newCol
leftExpandColumn n column@(UnboxedColumn bm col)
| n <= VG.length col = column
| otherwise =
let extra = n - VG.length col
origLen = VG.length col
newBm = case bm of
Nothing -> Just (buildBitmapFromNulls' n (VU.enumFromN 0 extra))
Just b ->
let nullPart = VU.replicate ((extra + 7) `shiftR` 3) 0
in Just (bitmapConcat extra nullPart origLen b)
newCol = runST $ do
mv <- VUM.new n
VU.imapM_ (\i x -> VUM.unsafeWrite mv (extra + i) x) col
VU.unsafeFreeze mv
in UnboxedColumn newBm newCol
{- | Concatenates two columns.
Returns Nothing if the columns are of different types.
-}
mappendColumns :: Column -> Column -> Either DataFrameException Column
mappendColumns left right = case (left, right) of
(MergedColumn _ _, _) -> mappendColumns (materializeMerged left) right
(_, MergedColumn _ _) -> mappendColumns left (materializeMerged right)
(PackedText _ _, _) -> mappendColumns (materializePacked left) right
(_, PackedText _ _) -> mappendColumns left (materializePacked right)
(BoxedColumn bmL l, BoxedColumn bmR r) -> case testEquality (typeOf l) (typeOf r) of
Just Refl ->
let newBm = case (bmL, bmR) of
(Nothing, Nothing) -> Nothing
(Just bl, Nothing) ->
Just
(bitmapConcat (VB.length l) bl (VB.length r) (allValidBitmap (VB.length r)))
(Nothing, Just br) ->
Just
(bitmapConcat (VB.length l) (allValidBitmap (VB.length l)) (VB.length r) br)
(Just bl, Just br) -> Just (bitmapConcat (VB.length l) bl (VB.length r) br)
in pure (BoxedColumn newBm (l <> r))
Nothing -> Left (mismatchErr (typeOf r) (typeOf l))
(UnboxedColumn bmL l, UnboxedColumn bmR r) -> case testEquality (typeOf l) (typeOf r) of
Just Refl ->
let newBm = case (bmL, bmR) of
(Nothing, Nothing) -> Nothing
(Just bl, Nothing) ->
Just
(bitmapConcat (VU.length l) bl (VU.length r) (allValidBitmap (VU.length r)))
(Nothing, Just br) ->
Just
(bitmapConcat (VU.length l) (allValidBitmap (VU.length l)) (VU.length r) br)
(Just bl, Just br) -> Just (bitmapConcat (VU.length l) bl (VU.length r) br)
in pure (UnboxedColumn newBm (l <> r))
Nothing -> Left (mismatchErr (typeOf r) (typeOf l))
_ -> Left (mismatchErr (typeOf right) (typeOf left))
where
mismatchErr ::
forall (x :: Type) (y :: Type). TypeRep x -> TypeRep y -> DataFrameException
mismatchErr ta tb =
withTypeable ta $
withTypeable tb $
TypeMismatchException
( MkTypeErrorContext
{ userType = Right ta
, expectedType = Right tb
, callingFunctionName = Just "mappendColumns"
, errorColumnName = Nothing
}
)
{- | Like 'mappendColumns' but also combines columns of different types by wrapping
values in 'Either' (e.g. @[1,2]@ and @["a","b"]@ become
@[Left 1, Left 2, Right "a", Right "b"]@).
-}
{- | O(n) Concatenate a list of same-type columns in a single allocation.
All columns must have the same constructor and element type (as they will
within a single Parquet column). Calls 'error' on mismatch.
-}
concatManyColumns :: [Column] -> Column
concatManyColumns [] = fromList ([] :: [Maybe Int])
concatManyColumns [c] = c
concatManyColumns all'
| any isMergedColumn all' =
concatManyColumns (map materializeMerged all')
| any isPackedText all' =
concatManyColumns (map materializePacked all')
concatManyColumns (c0 : cs) = case c0 of
BoxedColumn bm0 v0 ->
let getCol (BoxedColumn bm v) = case testEquality (typeOf v0) (typeOf v) of
Just Refl -> (bm, v)
Nothing -> error "concatManyColumns: BoxedColumn type mismatch"
getCol _ = error "concatManyColumns: column constructor mismatch"
rest = map getCol cs
allVecs = v0 : map snd rest
allBms = bm0 : map fst rest
newBm
| all isNothing allBms = Nothing
| otherwise =
let pairs = zip allVecs allBms
expandedBms = map (\(v, mb) -> fromMaybe (allValidBitmap (VB.length v)) mb) pairs
go b1 n1 b2 n2 = bitmapConcat n1 b1 n2 b2
concatBms [] = VU.empty
concatBms [(b, _v)] = b
concatBms ((b1, v1) : (b2, v2) : rest') =
let merged = go b1 (VB.length v1) b2 (VB.length v2)
in concatBms ((merged, v1 <> v2) : rest')
in Just $ concatBms (zip expandedBms allVecs)
in BoxedColumn newBm (VB.concat allVecs)
UnboxedColumn bm0 v0 ->
let getCol (UnboxedColumn bm v) = case testEquality (typeOf v0) (typeOf v) of
Just Refl -> (bm, v)
Nothing -> error "concatManyColumns: UnboxedColumn type mismatch"
getCol _ = error "concatManyColumns: column constructor mismatch"
rest = map getCol cs
allVecs = v0 : map snd rest
allBms = bm0 : map fst rest
newBm
| all isNothing allBms = Nothing
| otherwise =
let pairs = zip allVecs allBms
expandedBms = map (\(v, mb) -> fromMaybe (allValidBitmap (VU.length v)) mb) pairs
go b1 n1 b2 n2 = bitmapConcat n1 b1 n2 b2
concatBms [] = VU.empty
concatBms [(b, _)] = b
concatBms ((b1, v1) : (b2, v2) : rest') =
let merged = go b1 (VU.length v1) b2 (VU.length v2)
in concatBms ((merged, v1 <> v2) : rest')
in Just $ concatBms (zip expandedBms allVecs)
in UnboxedColumn newBm (VU.concat allVecs)
PackedText _ _ -> concatManyColumns (map materializePacked (c0 : cs))
MergedColumn _ _ -> concatManyColumns (map materializeMerged (c0 : cs))
mappendColumnsEither :: Column -> Column -> Column
mappendColumnsEither l@(MergedColumn _ _) r =
mappendColumnsEither (materializeMerged l) r
mappendColumnsEither l r@(MergedColumn _ _) =
mappendColumnsEither l (materializeMerged r)
mappendColumnsEither l@(PackedText _ _) r = mappendColumnsEither (materializePacked l) r
mappendColumnsEither l r@(PackedText _ _) = mappendColumnsEither l (materializePacked r)
mappendColumnsEither (BoxedColumn bmL left) (BoxedColumn bmR right) = case testEquality (typeOf left) (typeOf right) of
Nothing ->
BoxedColumn Nothing $ fmap Left left <> fmap Right right
Just Refl ->
let newBm = case (bmL, bmR) of
(Nothing, Nothing) -> Nothing
(Just bl, Nothing) ->
Just
( bitmapConcat
(VB.length left)
bl
(VB.length right)
(allValidBitmap (VB.length right))
)
(Nothing, Just br) ->
Just
( bitmapConcat
(VB.length left)
(allValidBitmap (VB.length left))
(VB.length right)
br
)
(Just bl, Just br) -> Just (bitmapConcat (VB.length left) bl (VB.length right) br)
in BoxedColumn newBm $ left <> right
mappendColumnsEither (UnboxedColumn bmL left) (UnboxedColumn bmR right) = case testEquality (typeOf left) (typeOf right) of
Nothing ->
BoxedColumn Nothing $
fmap Left (VG.convert left) <> fmap Right (VG.convert right)
Just Refl ->
let newBm = case (bmL, bmR) of
(Nothing, Nothing) -> Nothing
(Just bl, Nothing) ->
Just
( bitmapConcat
(VU.length left)
bl
(VU.length right)
(allValidBitmap (VU.length right))
)
(Nothing, Just br) ->
Just
( bitmapConcat
(VU.length left)
(allValidBitmap (VU.length left))
(VU.length right)
br
)
(Just bl, Just br) -> Just (bitmapConcat (VU.length left) bl (VU.length right) br)
in UnboxedColumn newBm $ left <> right
mappendColumnsEither (BoxedColumn _ left) (UnboxedColumn _ right) =
BoxedColumn Nothing $ fmap Left left <> fmap Right (VG.convert right)
mappendColumnsEither (UnboxedColumn _ left) (BoxedColumn _ right) =
BoxedColumn Nothing $ fmap Left (VG.convert left) <> fmap Right right
-------------------------------------------------------------------------------
-- Fused multi-column gather
-------------------------------------------------------------------------------
{- | Gather ONE in-bounds index vector through several columns in a single
parallel pass. Result columns are identical to @map (atIndicesStable ixs)@;
columns whose shape the fused kernel does not cover (bitmapped, boxed, merged,
unusual element types) fall back to per-column 'atIndicesStable'. All fused
outputs are backed by one shared deferred computation: forcing ANY of them
runs the single pass that fills ALL of them. The pass reads the index vector
once per block instead of once per column, and every iteration of a block
issues each column's independent random load back-to-back, so their cache/TLB
misses overlap instead of forming one latency chain per column (the aggregate
key materialization of a 1e8-group result was 6 sequential latency-bound
passes without this).
-}
atIndicesStableMulti :: VU.Vector Int -> [Column] -> [Column]
atIndicesStableMulti ixs cols =
let specs = map mgClassify cols
nFused = length [() | Just _ <- specs]
in if nFused < 2
then map (atIndicesStable ixs) cols
else
let fused = multiGatherRun ixs (catMaybes specs)
go [] _ = []
go (Nothing : ss) !k = atIndicesStable ixs (cols !! k) : go ss (k + 1)
go (Just _ : ss) !k =
let !r = mgRank k
in (fused VB.! r) : go ss (k + 1)
-- fused-output rank of column position k.
mgRank k = length [() | Just _ <- take k specs]
in go specs 0
-- | One fusable source column shape (all bitmap-free).
data MGSpec
= -- | Clean unboxed 'Int' payload.
MGInt !(VU.Vector Int)
| -- | Clean unboxed 'Double' payload.
MGDouble !(VU.Vector Double)
| {- | Packed text with an 'Int32' selector (base row count necessarily
fits 'Int32'); carries the payload for rebuilding and the canon flag.
-}
MGSel32 !PackedTextData !(VU.Vector Int32)
| -- | Packed text with an 'Int' selector and an 'Int32'-sized base.
MGSel64To32 !PackedTextData !(VU.Vector Int)
| -- | Packed text with an 'Int' selector and a wide base.
MGSel64To64 !PackedTextData !(VU.Vector Int)
mgClassify :: Column -> Maybe MGSpec
mgClassify (UnboxedColumn Nothing (v :: VU.Vector a)) =
case testEquality (typeRep @a) (typeRep @Int) of
Just Refl -> Just (MGInt v)
Nothing -> case testEquality (typeRep @a) (typeRep @Double) of
Just Refl -> Just (MGDouble v)
Nothing -> Nothing
mgClassify (PackedText Nothing p) = case ptSel p of
Just (Sel32 s) -> Just (MGSel32 p s)
Just (Sel64 s)
| offCount (ptOffsets p) - 1 <= mgInt32Max -> Just (MGSel64To32 p s)
| otherwise -> Just (MGSel64To64 p s)
Nothing -> Nothing
mgClassify _ = Nothing
mgInt32Max :: Int
mgInt32Max = fromIntegral (maxBound :: Int32)
{- | Run the fused pass over the fusable specs; element @r@ of the result is
spec @r@'s gathered column. Deferred as one shared thunk (see
'atIndicesStableMulti'). Pure w.r.t. its immutable inputs, so the
'unsafePerformIO' is safe.
-}
multiGatherRun :: VU.Vector Int -> [MGSpec] -> VB.Vector Column
multiGatherRun ixs specs = unsafePerformIO $ do
let !n = VU.length ixs
opened <- mapM (mgOpen n ixs) specs
let fills = map fst opened
!block = 4096
worker !lo !hi
| lo >= hi = pure ()
| otherwise = do
let !e = min hi (lo + block)
mapM_ (\fill -> fill lo e) fills
worker e hi
parallelChunks_ parThreshold n worker
VB.fromList <$> mapM snd opened
{-# NOINLINE multiGatherRun #-}
{- | Allocate a spec's destination; return its block-fill action and its
finalizer. Selector gathers reproduce 'packedGather' exactly (composition
clamps against the source selector length and base row count); unboxed gathers
reproduce the unclamped 'parBackpermuteUnboxed'.
-}
mgOpen :: Int -> VU.Vector Int -> MGSpec -> IO (Int -> Int -> IO (), IO Column)
mgOpen n ixs spec = case spec of
MGInt v -> do
mv <- VUM.unsafeNew n
pure
( mgFillInt ixs v mv
, UnboxedColumn Nothing <$> VU.unsafeFreeze mv
)
MGDouble v -> do
mv <- VUM.unsafeNew n
pure
( mgFillDouble ixs v mv
, UnboxedColumn Nothing <$> VU.unsafeFreeze mv
)
MGSel32 p s -> do
mv <- VUM.unsafeNew n
pure
( mgFillSel32 ixs s (offCount (ptOffsets p) - 1) mv
, (\out -> PackedText Nothing p{ptSel = Just (Sel32 out)}) <$> VU.unsafeFreeze mv
)
MGSel64To32 p s -> do
mv <- VUM.unsafeNew n
pure
( mgFillSel64To32 ixs s (offCount (ptOffsets p) - 1) mv
, (\out -> PackedText Nothing p{ptSel = Just (Sel32 out)}) <$> VU.unsafeFreeze mv
)
MGSel64To64 p s -> do
mv <- VUM.unsafeNew n
pure
( mgFillSel64To64 ixs s (offCount (ptOffsets p) - 1) mv
, (\out -> PackedText Nothing p{ptSel = Just (Sel64 out)}) <$> VU.unsafeFreeze mv
)
mgFillInt ::
VU.Vector Int -> VU.Vector Int -> VUM.IOVector Int -> Int -> Int -> IO ()
mgFillInt ixs v dst lo hi = go lo
where
go !i
| i >= hi = pure ()
| otherwise = do
VUM.unsafeWrite dst i (VU.unsafeIndex v (VU.unsafeIndex ixs i))
go (i + 1)
{-# NOINLINE mgFillInt #-}
mgFillDouble ::
VU.Vector Int -> VU.Vector Double -> VUM.IOVector Double -> Int -> Int -> IO ()
mgFillDouble ixs v dst lo hi = go lo
where
go !i
| i >= hi = pure ()
| otherwise = do
VUM.unsafeWrite dst i (VU.unsafeIndex v (VU.unsafeIndex ixs i))
go (i + 1)
{-# NOINLINE mgFillDouble #-}
mgFillSel32 ::
VU.Vector Int ->
VU.Vector Int32 ->
Int ->
VUM.IOVector Int32 ->
Int ->
Int ->
IO ()
mgFillSel32 ixs s !base dst lo hi = go lo
where
!sn = VU.length s
go !i
| i >= hi = pure ()
| otherwise = do
let !j = VU.unsafeIndex ixs i
!out =
if j >= 0 && j < sn
then
let !r = fromIntegral (VU.unsafeIndex s j) :: Int
in if r >= 0 && r < base then fromIntegral r else -1
else -1
VUM.unsafeWrite dst i out
go (i + 1)
{-# NOINLINE mgFillSel32 #-}
mgFillSel64To32 ::
VU.Vector Int ->
VU.Vector Int ->
Int ->
VUM.IOVector Int32 ->
Int ->
Int ->
IO ()
mgFillSel64To32 ixs s !base dst lo hi = go lo
where
!sn = VU.length s
go !i
| i >= hi = pure ()
| otherwise = do
let !j = VU.unsafeIndex ixs i
!out =
if j >= 0 && j < sn
then
let !r = VU.unsafeIndex s j
in if r >= 0 && r < base then fromIntegral r else -1
else -1
VUM.unsafeWrite dst i out
go (i + 1)
{-# NOINLINE mgFillSel64To32 #-}
mgFillSel64To64 ::
VU.Vector Int -> VU.Vector Int -> Int -> VUM.IOVector Int -> Int -> Int -> IO ()
mgFillSel64To64 ixs s !base dst lo hi = go lo
where
!sn = VU.length s
go !i
| i >= hi = pure ()
| otherwise = do
let !j = VU.unsafeIndex ixs i
!out =
if j >= 0 && j < sn
then
let !r = VU.unsafeIndex s j
in if r >= 0 && r < base then r else -1
else -1
VUM.unsafeWrite dst i out
go (i + 1)
{-# NOINLINE mgFillSel64To64 #-}