Skip to content

Commit 451e1ac

Browse files
committed
Reimplement Atomic with GHC prim ops
This commit attempts to address issue haskell-github-trust#41 of tibbe/ekg-core by replaceing the C code for `Atomic` with GHC prim ops (`fetchAddIntArray`). However, we insist on a 64-bit counter, so if machine does not support 64-bit prim ops, we fall back to using an `IORef` and `atomicModifyIORefCAS`. The performance of the 64-bit prim ops implementation is somewhat slower than the existing C code, perhaps due to the additional conversion between `Int` and `Int64`.
1 parent 08f7e0c commit 451e1ac

File tree

2 files changed

+83
-23
lines changed

2 files changed

+83
-23
lines changed

Data/Atomic.hs

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
{-# LANGUAGE BangPatterns, ForeignFunctionInterface #-}
1+
{-# LANGUAGE BangPatterns #-}
2+
{-# LANGUAGE CPP #-}
3+
{-# LANGUAGE ForeignFunctionInterface #-}
4+
{-# LANGUAGE MagicHash #-}
5+
{-# LANGUAGE TypeApplications #-}
6+
{-# LANGUAGE UnboxedTuples #-}
27
-- | An atomic integer value. All operations are thread safe.
38
module Data.Atomic
49
(
@@ -12,33 +17,52 @@ module Data.Atomic
1217
, subtract
1318
) where
1419

15-
import Data.Int (Int64)
16-
import Foreign.ForeignPtr (ForeignPtr, mallocForeignPtr, withForeignPtr)
17-
import Foreign.Ptr (Ptr)
18-
import Foreign.Storable (poke)
1920
import Prelude hiding (read, subtract)
2021

22+
#include "MachDeps.h"
23+
24+
-- We want an atomic with at least 64 bits in order to avoid overflow.
25+
26+
#if WORD_SIZE_IN_BITS >= 64
27+
28+
-- If the machine word size is 64 bits, we can use GHC's atomic primops
29+
-- (`fetchAddIntArray`) to implement our atomic.
30+
--
31+
-- We pad to 64-bytes (an x86 cache line) to try to avoid false sharing.
32+
--
33+
-- Implementation note: We always make sure to interact with the
34+
-- `MutableByteArray` at element type `Int`.
35+
36+
import Control.Monad (void)
37+
import Control.Monad.Primitive (RealWorld)
38+
import Data.Atomics (fetchAddIntArray, fetchSubIntArray)
39+
import Data.Int (Int64)
40+
import Data.Primitive.ByteArray
41+
import Data.Primitive.MachDeps (sIZEOF_INT)
42+
import Control.Exception (assert)
43+
44+
sIZEOF_CACHELINE :: Int
45+
sIZEOF_CACHELINE = 64
46+
{-# INLINE sIZEOF_CACHELINE #-}
47+
2148
-- | A mutable, atomic integer.
22-
newtype Atomic = C (ForeignPtr Int64)
49+
newtype Atomic = C (MutableByteArray RealWorld)
2350

24-
-- | Create a new, zero initialized, atomic.
51+
-- | Create a new atomic.
2552
new :: Int64 -> IO Atomic
2653
new n = do
27-
fp <- mallocForeignPtr
28-
withForeignPtr fp $ \ p -> poke p n
29-
return $ C fp
54+
arr <- newAlignedPinnedByteArray sIZEOF_CACHELINE sIZEOF_CACHELINE
55+
writeByteArray @Int arr 0 (fromIntegral n)
56+
-- out of principle:
57+
assert (sIZEOF_INT < sIZEOF_CACHELINE) $
58+
pure (C arr)
3059

3160
read :: Atomic -> IO Int64
32-
read (C fp) = withForeignPtr fp cRead
33-
34-
foreign import ccall unsafe "hs_atomic_read" cRead :: Ptr Int64 -> IO Int64
61+
read (C arr) = fromIntegral <$> readByteArray @Int arr 0
3562

3663
-- | Set the atomic to the given value.
3764
write :: Atomic -> Int64 -> IO ()
38-
write (C fp) n = withForeignPtr fp $ \ p -> cWrite p n
39-
40-
foreign import ccall unsafe "hs_atomic_write" cWrite
41-
:: Ptr Int64 -> Int64 -> IO ()
65+
write (C arr) n = writeByteArray @Int arr 0 (fromIntegral n)
4266

4367
-- | Increase the atomic by one.
4468
inc :: Atomic -> IO ()
@@ -50,15 +74,49 @@ dec atomic = subtract atomic 1
5074

5175
-- | Increase the atomic by the given amount.
5276
add :: Atomic -> Int64 -> IO ()
53-
add (C fp) n = withForeignPtr fp $ \ p -> cAdd p n
77+
add (C arr) n = void $ fetchAddIntArray arr 0 (fromIntegral n)
5478

5579
-- | Decrease the atomic by the given amount.
5680
subtract :: Atomic -> Int64 -> IO ()
57-
subtract (C fp) n = withForeignPtr fp $ \ p -> cSubtract p n
81+
subtract (C arr) n = void $ fetchSubIntArray arr 0 (fromIntegral n)
5882

59-
-- | Increase the atomic by the given amount.
60-
foreign import ccall unsafe "hs_atomic_add" cAdd :: Ptr Int64 -> Int64 -> IO ()
83+
#else
84+
85+
-- If the machine word size less than 64 bits, we fall back to `IORef`s
86+
-- and `atomicModifyIORefCAS`. This is much slower.
87+
88+
import Data.Atomics (atomicModifyIORefCAS_)
89+
import Data.Int (Int64)
90+
import Data.IORef
91+
92+
-- | A mutable, atomic integer.
93+
newtype Atomic = C (IORef Int64)
94+
95+
-- | Create a new atomic.
96+
new :: Int64 -> IO Atomic
97+
new n = C <$> newIORef n
98+
99+
read :: Atomic -> IO Int64
100+
read (C ref) = readIORef ref
101+
102+
-- | Set the atomic to the given value.
103+
write :: Atomic -> Int64 -> IO ()
104+
write (C ref) = writeIORef ref
105+
106+
-- | Increase the atomic by one.
107+
inc :: Atomic -> IO ()
108+
inc atomic = add atomic 1
109+
110+
-- | Decrease the atomic by one.
111+
dec :: Atomic -> IO ()
112+
dec atomic = subtract atomic 1
61113

62114
-- | Increase the atomic by the given amount.
63-
foreign import ccall unsafe "hs_atomic_subtract" cSubtract
64-
:: Ptr Int64 -> Int64 -> IO ()
115+
add :: Atomic -> Int64 -> IO ()
116+
add (C ref) n = atomicModifyIORefCAS_ ref $ \x -> x+n
117+
118+
-- | Decrease the atomic by the given amount.
119+
subtract :: Atomic -> Int64 -> IO ()
120+
subtract (C ref) n = atomicModifyIORefCAS_ ref $ \x -> x-n
121+
122+
#endif

ekg-core.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ library
3838
build-depends:
3939
ghc-prim < 0.7,
4040
base >= 4.6 && < 4.15,
41+
atomic-primops ^>= 0.8.4,
4142
containers >= 0.5 && < 0.7,
4243
hashable >= 1.3.1.0 && < 1.4,
44+
primitive ^>= 0.7.1.0,
4345
text < 1.3,
4446
unordered-containers < 0.3
4547

0 commit comments

Comments
 (0)