Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions _control/skipped_benchmarks.csv
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ pandas,join,J1_1e9_NA_0_0,c6id.4xlarge
polars,join,J1_1e9_NA_0_0,c6id.4xlarge
pydatatable,join,J1_1e9_NA_0_0,c6id.4xlarge
spark,join,J1_1e9_NA_0_0,c6id.4xlarge
haskell,join,J1_1e7_NA_0_0,c6id.4xlarge
haskell,join,J1_1e7_NA_0_0,c6id.metal
haskell,join,J1_1e9_NA_0_0,c6id.4xlarge
haskell,join,J1_1e9_NA_0_0,c6id.metal

18 changes: 18 additions & 0 deletions haskell/BenchmarkCommon.hs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
Expand All @@ -10,6 +11,7 @@ module BenchmarkCommon (
writeLog,
writeToLogFile,
roundTo,
freshRun,
) where

import Control.Exception (SomeException, catch, evaluate)
Expand Down Expand Up @@ -59,6 +61,22 @@ getMemoryUsage = do
Just kb -> return (kb / 1024)
Nothing -> return 0.0

{- | Defeat cross-iteration result sharing in the two-run loop.

The benchmark times the SAME pure transform twice. Because the transform is a
pure function of loop-invariant inputs, GHC happily computes it once and shares
the forced result, so run=2 measured ~0.001s (a meaningless cache hit) in
Round 4. We thread the per-run number through a NOINLINE call so the optimizer
cannot prove the two applications are equal and float/CSE the result out of the
loop: each run is forced to recompute from scratch. The Int is otherwise inert
(it only selects the identical value), so results and checksums are unchanged.
-}
{-# NOINLINE freshRun #-}
freshRun :: Int -> (a -> b) -> a -> b
freshRun !n f x
| n < 0 = error "freshRun: unreachable"
| otherwise = f x

timeIt :: IO a -> IO (a, Double)
timeIt action = do
start <- getPOSIXTime
Expand Down
8 changes: 4 additions & 4 deletions haskell/exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ fi

[ -f "$HOME/.ghcup/env" ] && source "$HOME/.ghcup/env"

# Only install and set GHC if 9.6.7 isn't already the active version.
# Only install and set GHC if 9.12.2 isn't already the active version.
# Re-running these on every benchmark invocation emits warnings to stderr
# that then get flagged by validate_no_errors.sh.
if ! ghc --version 2>/dev/null | grep -q "9.6.7"; then
ghcup install ghc 9.6.7
ghcup set ghc 9.6.7
if ! ghc --version 2>/dev/null | grep -q "9.12.2"; then
ghcup install ghc 9.12.2
ghcup set ghc 9.12.2
fi

source ./haskell/ver-haskell.sh
Expand Down
116 changes: 95 additions & 21 deletions haskell/groupby-haskell.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@ import BenchmarkCommon
import Control.Arrow ((>>>))
import Control.Monad (forM_)
import Data.Functor ((<&>))
import qualified Data.List as L
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import qualified DataFrame as D
import DataFrame.Functions ((.=))
import qualified DataFrame.Functions as F
import DataFrame.IO.CSV (
ReadOptions (..),
TypeSpec (..),
defaultReadOptions,
)
import DataFrame.IO.CSV.Fast
import DataFrame.Internal.DataFrame (DataFrame)
import DataFrame.Internal.Expression (AggStrategy (..), Expr (..))
import DataFrame.Internal.Schema (schemaType)
import qualified DataFrame.Operations.Aggregation as D
import qualified DataFrame.Operations.Core as D
import qualified DataFrame.Operations.Subset as D
import qualified DataFrame.Operations.Transformations as D
import System.Environment (getEnv, lookupEnv)
import System.IO (BufferMode (..), hPutStrLn, hSetBuffering, stderr, stdout)

Expand Down Expand Up @@ -47,23 +61,25 @@ runBenchmark srcFile dataName machineType = do
v1 = F.col @Int "v1"
v2 = F.col @Int "v2"
v3 = F.col @Double "v3"
dv1 = F.toDouble v1
dv2 = F.toDouble v2

df <-
D.readCsvWithOpts
( D.defaultReadOptions
{ D.typeSpec =
D.SpecifyTypes
[ (F.name id1, D.schemaType @Text)
, (F.name id2, D.schemaType @Text)
, (F.name id3, D.schemaType @Text)
, (F.name id4, D.schemaType @Int)
, (F.name id5, D.schemaType @Int)
, (F.name id6, D.schemaType @Int)
, (F.name v1, D.schemaType @Int)
, (F.name v2, D.schemaType @Int)
, (F.name v3, D.schemaType @Double)
fastReadCsvWithOpts
( defaultReadOptions
{ typeSpec =
SpecifyTypes
[ (F.name id1, schemaType @Text)
, (F.name id2, schemaType @Text)
, (F.name id3, schemaType @Text)
, (F.name id4, schemaType @Int)
, (F.name id5, schemaType @Int)
, (F.name id6, schemaType @Int)
, (F.name v1, schemaType @Int)
, (F.name v2, schemaType @Int)
, (F.name v3, schemaType @Double)
]
D.NoInference
NoInference
}
)
srcFile
Expand Down Expand Up @@ -158,6 +174,41 @@ runBenchmark srcFile dataName machineType = do
(D.groupBy [F.name id3] >>> D.aggregate ["diff" .= F.maximum v1 - F.minimum v2])
(\res -> [chkSumInt "diff" res])

-- Q8: Largest two v3 by id6.
-- Polars takes top_k(2) per group then explodes and sums; the sum of the
-- exploded values equals the sum of (max + 2nd-max) per group, which is
-- what 'top2Sum' computes per group. Checksum parity is preserved.
runQuestion
config
df
"largest two v3 by id6"
(D.groupBy [F.name id6] >>> D.aggregate ["largest2_v3" .= top2Sum v3])
(\res -> [chkSumDbl "largest2_v3" res])

-- Q9: Regression (r^2 of v1 vs v2) by id2, id4.
-- corr(v1,v2)^2 from the per-group sums of v1, v2, v1*v2, v1^2, v2^2 and the
-- group count, then summed. Matches polars pl.corr(...)**2 then .sum().
runQuestion
config
( D.derive
"v2v2"
(dv2 * dv2)
(D.derive "v1v1" (dv1 * dv1) (D.derive "v1v2" (dv1 * dv2) df))
)
"regression v1 v2 by id2 id4"
( D.groupBy [F.name id2, F.name id4]
>>> D.aggregate
[ "n" .= F.count v1
, "sx" .= F.sum dv1
, "sy" .= F.sum dv2
, "sxy" .= F.sum (F.col @Double "v1v2")
, "sxx" .= F.sum (F.col @Double "v1v1")
, "syy" .= F.sum (F.col @Double "v2v2")
]
>>> D.derive "r2" r2Expr
)
(\res -> [chkSumDbl "r2" res])

-- Q10: Sum v3 count by id1:id6
runQuestion
config
Expand All @@ -173,15 +224,15 @@ runBenchmark srcFile dataName machineType = do

runQuestion ::
BenchConfig ->
D.DataFrame ->
DataFrame ->
String ->
(D.DataFrame -> D.DataFrame) ->
(D.DataFrame -> [Double]) ->
(DataFrame -> DataFrame) ->
(DataFrame -> [Double]) ->
IO ()
runQuestion cfg inputDF qLabel transform chkFn = do
forM_ [1, 2] $ \runNum -> do
(resultDF, calcTime) <- timeIt $ do
let result = transform inputDF
let result = freshRun runNum transform inputDF
print result
return result
memUsage <- getMemoryUsage
Expand All @@ -193,14 +244,37 @@ runQuestion cfg inputDF qLabel transform chkFn = do
writeLog cfg qLabel outRows outCols runNum calcTime memUsage chkValues chkTime
putStrLn $ qLabel ++ " completed."

chkSumInt :: String -> D.DataFrame -> Double
chkSumInt :: String -> DataFrame -> Double
chkSumInt col df =
case D.columnAsIntVector (F.col @Int (T.pack col)) df of
Right vec -> fromIntegral $ VU.sum vec
Left _ -> 0.0

chkSumDbl :: String -> D.DataFrame -> Double
chkSumDbl :: String -> DataFrame -> Double
chkSumDbl col df =
case D.columnAsDoubleVector (F.col @Double (T.pack col)) df of
Right vec -> VU.sum vec
Left _ -> 0.0

-- | Per-group sum of the two largest values (top_k(2) then sum, like polars Q8).
top2Sum :: Expr Double -> Expr Double
top2Sum = Agg (CollectAgg "top2Sum" f)
where
f :: V.Vector Double -> Double
f v = Prelude.sum (take 2 (L.sortBy (flip compare) (V.toList v)))

{- | Squared Pearson correlation of v1,v2 from per-group moment sums (polars Q9).
Degenerate groups (zero variance) contribute 0 rather than poisoning the sum
with NaN; the 1e7 data has no such groups so the checksum is unaffected.
-}
r2Expr :: Expr Double
r2Expr =
let n = F.toDouble (F.col @Int "n")
sx = F.col @Double "sx"
sy = F.col @Double "sy"
sxy = F.col @Double "sxy"
sxx = F.col @Double "sxx"
syy = F.col @Double "syy"
num = n * sxy - sx * sy
den = (n * sxx - sx * sx) * (n * syy - sy * sy)
in If (F.gt den (Lit 0)) ((num * num) / den) (Lit 0)
40 changes: 21 additions & 19 deletions haskell/haskell-benchmark.cabal
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
cabal-version: 2.4
name: haskell-benchmark
version: 0.1.0.0
build-type: Simple
cabal-version: >=2.0

executable groupby-haskell
main-is: groupby-haskell.hs
hs-source-dirs: .
other-modules: BenchmarkCommon
common df-deps
build-depends: base >= 4.7 && < 5
, bytestring >= 0.11 && < 0.13
, cassava >= 0.1 && < 1
, dataframe ^>= 1.1
, text >= 1.2
, dataframe-core ^>= 1.0.2
, dataframe-parsing ^>= 1.0
, dataframe-csv ^>= 1.0
, dataframe-operations ^>= 1.1
, dataframe-fastcsv ^>= 1.1
, text >= 2.1
, vector >= 0.12
, time >= 1.9
, process >= 1.6
Expand All @@ -20,19 +21,20 @@ executable groupby-haskell
default-language: Haskell2010
ghc-options: -O2 -threaded -rtsopts -with-rtsopts=-N

executable groupby-haskell
import: df-deps
main-is: groupby-haskell.hs
hs-source-dirs: .
other-modules: BenchmarkCommon

executable join-haskell
import: df-deps
main-is: join-haskell.hs
hs-source-dirs: .
other-modules: BenchmarkCommon
build-depends: base >= 4.7 && < 5
, bytestring >= 0.11 && < 0.13
, cassava >= 0.1 && < 1
, dataframe ^>= 1.1
, text >= 1.2
, vector >= 0.12
, time >= 1.9
, process >= 1.6
, directory >= 1.3
, unix
default-language: Haskell2010
ghc-options: -O2 -threaded -rtsopts -with-rtsopts=-N

executable timeq
import: df-deps
main-is: timeq.hs
hs-source-dirs: .
build-depends: deepseq
Loading
Loading