symbolize 1.0.2.3 → 1.0.2.4
raw patch · 5 files changed
+82/−13 lines, 5 filesdep +tasty-benchdep +vectorPVP ok
version bump matches the API change (PVP)
Dependencies added: tasty-bench, vector
API changes (from Hackage documentation)
Files
- CHANGELOG.md +5/−0
- benchmarks/SymbolizeBench.hs +33/−0
- src/Symbolize/SymbolTable.hs +9/−9
- symbolize.cabal +33/−2
- test/suite/SymbolizeTest.hs +2/−2
CHANGELOG.md view
@@ -8,6 +8,11 @@ ## Unreleased +## 1.0.2.4++- Fix too eager creation of weak pointers, resulting in many needless allocations. This greatly reduces memory pressure and improves speed when inserting many symbols in a short amount of time! +- Only calculate the SipHash hash of an incoming text _once_ when inserting. (c.f. [#8](https://github.com/Qqwy/haskell-symbolize/issues/8))+ ## 1.0.2.3 - Swaps the internal usage of lists in the global symbol table for `NonEmpty` lists, since they will never be empty. (PR [#5](https://github.com/Qqwy/haskell-symbolize/pull/5)). Thank you, @Bodigrim!
+ benchmarks/SymbolizeBench.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE GHC2021 #-}+{-# LANGUAGE OverloadedLists #-}+import Test.Tasty.Bench++import Symbolize qualified++import Data.Text (Text)+import Data.Text qualified as Text+import Data.Vector (Vector)+import Data.Vector qualified as V++mkTexts :: IO (Vector Text)+mkTexts = do+ let vec = Text.pack . show @Int <$> [0..3200]+ pure vec++roundtripMany :: Vector Text -> Vector Text+roundtripMany = fmap (\val -> Symbolize.unintern $! Symbolize.intern $! val)+++main :: IO ()+main = defaultMain+ [ env mkTexts $ \texts ->+ bgroup "intern/unintern roundtrip, no duplicates"+ [ bench "10" $ nf roundtripMany $! V.take 10 texts+ , bench "100" $ nf roundtripMany $! V.take 100 texts+ , bench "200" $ nf roundtripMany $! V.take 200 texts+ , bench "400" $ nf roundtripMany $! V.take 400 texts+ , bench "800" $ nf roundtripMany $! V.take 800 texts+ , bench "1600" $ nf roundtripMany $! V.take 1600 texts+ , bench "3200" $ nf roundtripMany $! V.take 3200 texts+ ]+ ]
src/Symbolize/SymbolTable.hs view
@@ -79,26 +79,27 @@ {-# INLINE insertGlobal #-} insertGlobal ba# = do GlobalSymbolTable gsymtab sipkey <- globalSymbolTable- let !key = calculateHash sipkey ba#+ let !hash = calculateHash sipkey ba# -- SAFETY: If the table IORef contested, -- this might trigger `weak` creation for the same bytestring from multiple threads -- at the same time. -- But finalization is idempotent, and only when a thread finally wins the Compare-and-Swap -- will its `weak` pointer be inserted (or alternatively another previously-inserted `ba` returned). -- So once this function returns, we can be sure we've returned a deduplicated ByteArray- !weak <- mkWeakSymbol ba# (removeGlobal key) IORef.atomicModifyIORef' gsymtab $ \table ->- case lookup ba# sipkey table of+ case lookup ba# hash table of Just ba -> (table, ba) Nothing ->- (insert key weak table, ByteArray ba#)+ let !weak = Accursed.accursedUnutterablePerformIO (mkWeakSymbol ba# (removeGlobal hash))+ in (insert hash weak table, ByteArray ba#) lookupGlobal :: ByteArray# -> IO (Maybe ByteArray) {-# INLINE lookupGlobal #-} lookupGlobal ba# = do GlobalSymbolTable gsymtab sipkey <- globalSymbolTable table <- IORef.readIORef gsymtab- pure (lookup ba# sipkey table)+ let hash = calculateHash sipkey ba#+ pure (lookup ba# hash table) removeGlobal :: Hash -> IO () {-# INLINE removeGlobal #-}@@ -113,11 +114,10 @@ let table' = IntMap.alter (Just . maybe (pure weak) (NonEmpty.cons weak)) (hashToInt key) table in SymbolTable table' -lookup :: ByteArray# -> SipHash.SipKey -> SymbolTable -> Maybe ByteArray+lookup :: ByteArray# -> Hash -> SymbolTable -> Maybe ByteArray {-# INLINE lookup #-}-lookup ba# sipkey (SymbolTable table) = do- let !key = calculateHash sipkey ba#- weaks <- IntMap.lookup (hashToInt key) table+lookup ba# hash (SymbolTable table) = do+ weaks <- IntMap.lookup (hashToInt hash) table Foldable.find (\other -> other == ByteArray ba#) (aliveWeaks weaks) remove :: Hash -> SymbolTable -> SymbolTable
symbolize.cabal view
@@ -1,11 +1,11 @@ cabal-version: 2.2 --- This file has been generated from package.yaml by hpack version 0.35.2.+-- This file has been generated from package.yaml by hpack version 0.37.0. -- -- see: https://github.com/sol/hpack name: symbolize-version: 1.0.2.3+version: 1.0.2.4 synopsis: Efficient global Symbol table, with Garbage Collection. description: Symbols, also known as Atoms or Interned Strings, are a common technique to reduce memory usage and improve performance when using many small strings:@@ -146,4 +146,35 @@ , tasty-hunit , text >=2.0 && <2.2 , text-short >=0.1.0 && <0.2+ default-language: Haskell2010++benchmark symbolize-bench+ type: exitcode-stdio-1.0+ main-is: SymbolizeBench.hs+ other-modules:+ Paths_symbolize+ autogen-modules:+ Paths_symbolize+ hs-source-dirs:+ benchmarks+ default-extensions:+ BangPatterns+ OverloadedStrings+ DeriveAnyClass+ TypeApplications+ NamedFieldPuns+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -with-rtsopts=-A32m -fproc-alignment=64+ build-depends:+ base >=4.7 && <5+ , binary >=0.8.9 && <0.9+ , bytestring >=0.11.0 && <0.13+ , containers >=0.6.0 && <0.8+ , deepseq >=1.4.0 && <1.6+ , hashable >=1.4.0 && <1.6+ , random >=1.2 && <2+ , symbolize+ , tasty-bench+ , text >=2.0 && <2.2+ , text-short >=0.1.0 && <0.2+ , vector default-language: Haskell2010
test/suite/SymbolizeTest.hs view
@@ -13,7 +13,7 @@ -- import Test.Tasty.HUnit hprop_symbolTableIsIdempotent :: Property-hprop_symbolTableIsIdempotent = withTests 1000 $ property $ do+hprop_symbolTableIsIdempotent = withTests 2000 $ property $ do texts <- forAll $ Gen.list (Range.linear 0 200) (Gen.text (Range.linear 0 20) Gen.unicode) let !symbols = fmap Symbolize.intern texts annotateShow (fmap Data.Hashable.hash symbols)@@ -22,7 +22,7 @@ texts2 === texts hprop_concurrentAccessDoesNotCorruptTable :: Property-hprop_concurrentAccessDoesNotCorruptTable = withTests 500 $ property $ do+hprop_concurrentAccessDoesNotCorruptTable = withTests 2000 $ property $ do let numCores = 8 texts <- forAll $ Gen.list (Range.linear 0 200) (Gen.text (Range.linear 0 20) Gen.unicode) results <- liftIO $ Control.Concurrent.Async.forConcurrently [(1 :: Integer) .. numCores] $ \_ -> do