From d618b767a892af278466e75299f232c85479d71d Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Wed, 12 Aug 2026 14:21:35 -0300 Subject: [PATCH 1/3] Faster Generically derived row decoders This improves our "Record Stream" benchmarks by ~8.2%, even if total memory allocated goes up a bit. --- Runfile | 6 ++++ hpgsql-benchmarks/src/Main.hs | 2 ++ hpgsql-tests/RowDecoderGhcCore.hs | 53 +++++++++++++++++++++++++++++++ hpgsql-tests/hpgsql-tests.cabal | 1 + hpgsql/src/Hpgsql/Encoding.hs | 9 +++++- 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 hpgsql-tests/RowDecoderGhcCore.hs diff --git a/Runfile b/Runfile index bcb45a5..5592c6a 100644 --- a/Runfile +++ b/Runfile @@ -104,3 +104,9 @@ tests-compat: cabal build hpgsql-simple-compat-tests nix-shell -A "shellPg${pg}" ./default.nix --run "./scripts/run-hpgsql-simple-compat-tests-db-internal.sh $TARGS" done + +ghc-core: + set -eo pipefail + rm -f dist-newstyle/build/x86_64-linux/ghc-9.10.3/hpgsql-tests-0.1.0.0/x/hpgsql-tests/build/hpgsql-tests/hpgsql-tests-tmp/RowDecoderGhcCore.thr.dump-simpl + cabal build hpgsql-tests 1>&2 + cat dist-newstyle/build/x86_64-linux/ghc-9.10.3/hpgsql-tests-0.1.0.0/x/hpgsql-tests/build/hpgsql-tests/hpgsql-tests-tmp/RowDecoderGhcCore.thr.dump-simpl diff --git a/hpgsql-benchmarks/src/Main.hs b/hpgsql-benchmarks/src/Main.hs index 081cc9f..139bd82 100644 --- a/hpgsql-benchmarks/src/Main.hs +++ b/hpgsql-benchmarks/src/Main.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -ddump-simpl -ddump-to-file #-} + module Main where import Control.Concurrent.Async (mapConcurrently) diff --git a/hpgsql-tests/RowDecoderGhcCore.hs b/hpgsql-tests/RowDecoderGhcCore.hs new file mode 100644 index 0000000..e53d8a5 --- /dev/null +++ b/hpgsql-tests/RowDecoderGhcCore.hs @@ -0,0 +1,53 @@ +{-# OPTIONS_GHC -ddump-simpl -ddump-to-file #-} + +-- | +-- This is not a real test module. It's just a type deriving `FromPgRow` +-- so we can look at GHC Core output. +module RowDecoderGhcCore where + +import Data.Int (Int64) +import Data.Text (Text) +import Data.Time (Day, UTCTime) +import GHC.Generics (Generic) +import Hpgsql.Encoding (FromPgRow (..), fieldDecoder, genericFromPgRow, singleField) + +data BenchRow = BenchRow + { brId :: !Int, + brDate1 :: !Day, + brDate2 :: !Day, + brTimestamp1 :: !UTCTime, + brTimestamp2 :: !UTCTime, + brText1 :: !Text, + brText2 :: !Text, + brDouble1 :: !Double, + brDouble2 :: !Double, + brMaybeInt :: !(Maybe Int), + brMaybeText :: !(Maybe Text), + brMaybeDouble :: !(Maybe Double), + brMaybeDay :: !(Maybe Day) + } + +-- Generically deriving section. + +deriving instance Generic BenchRow + +instance FromPgRow BenchRow where + rowDecoder = genericFromPgRow + +-- Hand-written applicative style deriving section. +-- instance FromPgRow BenchRow where +-- rowDecoder = +-- SomeRecord +-- <$> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder +-- <*> singleField fieldDecoder diff --git a/hpgsql-tests/hpgsql-tests.cabal b/hpgsql-tests/hpgsql-tests.cabal index 4a16b6b..8528097 100644 --- a/hpgsql-tests/hpgsql-tests.cabal +++ b/hpgsql-tests/hpgsql-tests.cabal @@ -36,6 +36,7 @@ executable hpgsql-tests ParsingSpec PipelineSpec PreparedStatementsSpec + RowDecoderGhcCore SqlQuasiquoterSpec TestUtils ThreadSafetySpec diff --git a/hpgsql/src/Hpgsql/Encoding.hs b/hpgsql/src/Hpgsql/Encoding.hs index f653109..6a1e771 100644 --- a/hpgsql/src/Hpgsql/Encoding.hs +++ b/hpgsql/src/Hpgsql/Encoding.hs @@ -76,6 +76,7 @@ import qualified Data.ByteString.Char8 as BSC import qualified Data.ByteString.Lazy as LBS import Data.CaseInsensitive (CI) import qualified Data.CaseInsensitive as CI +import Data.Coerce (coerce) import Data.Fixed (divMod') import Data.Functor.Contravariant (Contravariant (..)) import Data.Int (Int16, Int32, Int64) @@ -1192,13 +1193,19 @@ class ProductTypeDecoder f where genRowDecoder :: RowDecoder (f a) instance (ProductTypeDecoder a, ProductTypeDecoder b) => ProductTypeDecoder (a :*: b) where + {-# INLINE genRowDecoder #-} genRowDecoder = (:*:) <$> genRowDecoder <*> genRowDecoder instance (ProductTypeDecoder f) => ProductTypeDecoder (M1 a c f) where + {-# INLINE genRowDecoder #-} genRowDecoder = M1 <$> genRowDecoder instance (FromPgField a) => ProductTypeDecoder (K1 r a) where - genRowDecoder = fmap K1 $ singleField $ fieldDecoder @a + {-# INLINE genRowDecoder #-} + -- coercing instead of fmap reduces memory usage, apparently + -- by reducing (unnecessary) closures in the final row decoder, + -- as per looking at GHC Core + genRowDecoder = coerce $ singleField $ fieldDecoder @a genericToPgRow :: forall a. (Generic a, ProductTypeEncoder (Rep a)) => RowEncoder a genericToPgRow = contramap from genRowEncoder From b1e17fe676f1d1c0ec424fca2b96350f58ecedd8 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 23 Aug 2026 09:54:11 -0300 Subject: [PATCH 2/3] Update benchmarks --- BENCHMARKS.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 8a741e7..474bf7c 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -31,9 +31,9 @@ This runs with 2 concurrent queries, 10 times over: This benchmark is unfair towards both hpgsql and postgresql-simple because the row decoder is Generically derived for them while it is hand-written for hasql. ```csv -postgresql-simple Record List (100000 rows),12.07,142.59M,99.3 -hasql Record List (100000 rows),6.369,142.48M,79.0 -hpgsql Record List (100000 rows),3.908,72.07M,119.9 +postgresql-simple Record List (100000 rows),12.23,142.52M,91.3 +hasql Record List (100000 rows),6.119,142.48M,78.5 +hpgsql Record List (100000 rows),3.719,72.07M,120.2 ``` ### Materializing 100_000 rows with 13 columns each into a List of Tuples @@ -55,9 +55,9 @@ However, Hpgsql's implementation streams directly from the socket while the othe it might not be a fair comparison in terms of implementation (e.g. you can advance multiple cursors simultaneously, but not hpgsql's Streamed-from-socket streams). ```csv -streaming-postgresql-simple Record Stream (100000 rows),13.59,73.34M,0.0 -postgresql-simple Record fold (100000 rows),13.37,77.84M,0.0 -hpgsql Record Stream (100000 rows),1.307,72.07M,0.0 +streaming-postgresql-simple Record Stream (100000 rows),13.05,73.30M,0.0 +postgresql-simple Record fold (100000 rows),12.23,76.22M,0.0 +hpgsql Record Stream (100000 rows),1.117,72.07M,0.0 ``` ### Streaming 100_000 rows with 13 columns as Tuples From 61cb47882a825a5c5edf5c809d4a21eca1dd8b1a Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 23 Aug 2026 09:54:25 -0300 Subject: [PATCH 3/3] Small improvement to `run ghc-core` --- Runfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Runfile b/Runfile index 5592c6a..406b180 100644 --- a/Runfile +++ b/Runfile @@ -107,6 +107,5 @@ tests-compat: ghc-core: set -eo pipefail - rm -f dist-newstyle/build/x86_64-linux/ghc-9.10.3/hpgsql-tests-0.1.0.0/x/hpgsql-tests/build/hpgsql-tests/hpgsql-tests-tmp/RowDecoderGhcCore.thr.dump-simpl cabal build hpgsql-tests 1>&2 cat dist-newstyle/build/x86_64-linux/ghc-9.10.3/hpgsql-tests-0.1.0.0/x/hpgsql-tests/build/hpgsql-tests/hpgsql-tests-tmp/RowDecoderGhcCore.thr.dump-simpl