jvm-batching 0.1.2 → 0.2.0
raw patch · 6 files changed
+238/−64 lines, 6 filesdep ~inline-javadep ~jnidep ~jvm
Dependency ranges changed: inline-java, jni, jvm
Files
- README.md +16/−0
- jvm-batching.cabal +6/−4
- src/main/haskell/Language/Java/Batching.hs +151/−50
- src/main/java/io/tweag/jvm/batching/BatchWriters.java +35/−7
- src/test/haskell/Language/Java/BatchingSpec.hs +19/−0
- src/test/haskell/Main.hs +11/−3
README.md view
@@ -2,6 +2,22 @@ Provides batched marshalling of values between Java and Haskell. +It provides reify and reflect instances for vectors, which marshal+values in batches, which is more efficient than marshalling values+one at a time.++```Haskell+instance (Interpretation a, BatchReify a) => Reify (V.Vector a) where+ ...++instance (Interpretation a, BatchReflect a) => Reflect (V.Vector a) where+ ...+```++See the documentation in+[Language.Hava.Batching](src/main/haskell/Language/Java/Batching.hs)+for an overview on how the implementation works.+ ## Using it as a dependency Add `jvm-batching` to the list of dependencies in your .cabal file.
jvm-batching.cabal view
@@ -1,5 +1,5 @@ name: jvm-batching-version: 0.1.2+version: 0.2.0 synopsis: Provides batched marshalling of values between Java and Haskell. description: Please see README.md. homepage: http://github.com/tweag/inline-java/tree/master/jvm-batching#readme@@ -43,9 +43,9 @@ base >= 4.7 && < 5, bytestring, distributed-closure >= 0.3.5,- jni >= 0.7.0 && <0.8,- jvm >= 0.5.0 && <0.6,- inline-java >= 0.7 && <0.10,+ jni >= 0.8.0 && <0.9,+ jvm >= 0.6.0 && <0.7,+ inline-java >= 0.10 && <0.11, singletons >= 2.2, text, vector@@ -63,11 +63,13 @@ bytestring, hspec, inline-java,+ jni, jvm, jvm-batching, text, vector default-language: Haskell2010+ ghc-options: -threaded cpp-options: -DHSPEC_DISCOVER=hspec-discover benchmark micro-benchmarks
src/main/haskell/Language/Java/Batching.hs view
@@ -21,6 +21,16 @@ -- | This module provides composable batched marshalling. --+-- It provides reify and reflect instances for vectors, which marshal+-- values in batches, which is more efficient than marshalling values+-- one at a time.+--+-- > instance (Interpretation a, BatchReify a) => Reify (V.Vector a) where+-- > ...+--+-- > instance (Interpretation a, BatchReflect a) => Reflect (V.Vector a) where+-- > ...+-- -- === Batching -- -- Calls to Java methods via JNI are slow in general. Marshalling an array of@@ -109,6 +119,33 @@ -- the 'ByteString's in the batch. The other array contains the offset of each -- vector in the resulting array. See 'ArrayBatch'. --+-- === null values in batches+--+-- Suppose we have an array of pairs in Java:+--+-- > new Tuple2<Integer,Integer>[] { null, new Tuple2(1, 2), new Tuple2(3, 4) }+--+-- In Haskell we can represent it using the 'Nullable' type+--+-- > [ Null, NotNull (1, 2), NotNull (3, 4) ]+--+-- However, we can't use the batch type we defined above for @Tuple2@,+-- because there is no way to store a null value in a pair of primitive arrays.+--+-- For this reason we define a special batch type for values that can be null.+-- We place a boolean array together with the regular batch of values.+--+-- > type Batch (Nullable a) =+-- > 'Class "io.tweag.jvm.batching.Tuple2" <>+-- > '[ 'Array ('Prim "boolean")+-- > , ArrayBatch (Batch a)+-- > ]+--+-- The positions in the batch for type @a@ are only meaningful if the boolean+-- array holds @True@ in the corresponding positions, the remaining positions+-- are regarded to hold null values.+--+ module Language.Java.Batching ( Batchable(..) , BatchReify(..)@@ -175,27 +212,42 @@ newBatchWriter _ = generic <$> [java| new BatchWriters.ObjectBatchWriter() |] -- | Reifies the values in a batch of type @Batch a@.- -- Gets the batch and the amount of elements it contains.- reifyBatch :: J (Batch a) -> Int32 -> IO (V.Vector a)+ --+ -- Gets the batch, the amount of elements it contains and a predicate that+ -- indicates which positions of the batch to reify. The value at position i+ -- in the returned vector holds bottom if @p i@ is False.+ --+ -- The predicate is used by instances of BatchReify to control which+ -- positions are to be reified in the batches of the components of a+ -- composite type whose values are allowed to be null.+ reifyBatch :: J (Batch a) -> Int32 -> (Int -> Bool) -> IO (V.Vector a) -- The default implementation makes calls to the JVM for each element in the -- batch. default reifyBatch :: (Reify a, Batch a ~ 'Array (Interp a))- => J (Batch a) -> Int32 -> IO (V.Vector a)- reifyBatch jxs size =- V.generateM (fromIntegral size) $ \i ->- getObjectArrayElement jxs (fromIntegral i) >>= reify . unsafeCast+ => J (Batch a) -> Int32 -> (Int -> Bool) -> IO (V.Vector a)+ reifyBatch jxs size p =+ V.generateM (fromIntegral size) $ \i ->+ if p i then+ getObjectArrayElement jxs (fromIntegral i) >>= reify . unsafeCast+ else+ return $ error "default reifyBatch: reification skipped." -- | Helper for reifying batches of primitive types reifyPrimitiveBatch :: Storable a => (J ('Array ty) -> IO (Ptr a)) -> (J ('Array ty) -> Ptr a -> IO ())- -> J ('Array ty) -> Int32 -> IO (V.Vector a)-reifyPrimitiveBatch getArrayElements releaseArrayElements jxs size = do- bracket (getArrayElements jxs) (releaseArrayElements jxs)- $ V.generateM (fromIntegral size) . peekElemOff+ -> J ('Array ty)+ -> Int32+ -> (Int -> Bool)+ -> IO (V.Vector a)+reifyPrimitiveBatch getArrayElements releaseArrayElements jxs size p = do+ bracket (getArrayElements jxs) (releaseArrayElements jxs) $ \arr ->+ V.generateM (fromIntegral size) $ \i ->+ if p i then peekElemOff arr i+ else return $ error "default reifyBatch: reification skipped." -- | Batches of arrays of variable length --@@ -227,8 +279,9 @@ -> (Int -> Int -> a -> IO b) -- ^ slice at a given offset of given length of some array a -> J (ArrayBatch ty) -> Int32+ -> (Int -> Bool) -> IO (V.Vector b)-reifyArrayBatch reifyB slice batch0 batchSize = do+reifyArrayBatch reifyB slice batch0 batchSize p = do let batch = unsafeUngeneric batch0 arrayEnds <- reifyArrayOffsets batch arrayValues <- reifyArrayValues arrayEnds batch@@ -261,10 +314,18 @@ -> Int -- ^ index of the position to write in the output vector -> IO Int32 -- ^ offset of the next slice to read writeSliceToVector output arrayEnds arrayValues offset i = do- slice (fromIntegral offset)- (fromIntegral $ arrayEnds VS.! i - offset) arrayValues- >>= VM.unsafeWrite output i- return $ arrayEnds VS.! i+ -- Account for the posibility of @arrayEnds ! i@ to be 0+ -- in case the position in the batch has been left uninitialized.+ let nextOffset = max (arrayEnds VS.! i) offset+ if p i then do+ slice (fromIntegral offset)+ (fromIntegral $ nextOffset - offset) arrayValues+ >>= VM.unsafeWrite output i+ return nextOffset+ else do+ VM.unsafeWrite output i $+ error "reifyPrimitiveArrayBatch: reification skipped."+ return offset -- | A class for batching reflection of values. --@@ -354,12 +415,15 @@ instance BatchReify Bool where newBatchWriter _ = [java| new BatchWriters.BooleanBatchWriter() |]- reifyBatch jxs size = do+ reifyBatch jxs size p = do let toBool w = if w == 0 then False else True bracket (getBooleanArrayElements jxs) (releaseBooleanArrayElements jxs) $- \arr -> V.generateM (fromIntegral size)- ((toBool <$>) . peekElemOff arr)+ \arr -> V.generateM (fromIntegral size) $ \i ->+ if p i then+ toBool <$> peekElemOff arr i+ else+ return $ error "reifyBatch Bool: reification skipped." instance Batchable CChar where type Batch CChar = 'Array ('Prim "byte")@@ -596,52 +660,89 @@ instance (Interpretation a, BatchReify a) => Reify (V.Vector a) where- reify jv = do- _batcher <- unsafeUngeneric <$> newBatchWriter (Proxy :: Proxy a)- n <- getArrayLength jv- let _jvo = arrayUpcast jv- batch <- [java| {- $_batcher.start($n);- for(int i=0;i<$_jvo.length;i++)- $_batcher.set(i, $_jvo[i]);- return $_batcher.getBatch();- } |]- reifyBatch (fromObject batch) n+ reify jv =+ withLocalRef (unsafeUngeneric <$> newBatchWriter (Proxy :: Proxy a))+ $ \_batcher -> do+ n <- getArrayLength jv+ let _jvo = arrayUpcast jv+ withLocalRef [java| {+ $_batcher.start($n);+ for(int i=0;i<$_jvo.length;i++)+ $_batcher.set(i, $_jvo[i]);+ return $_batcher.getBatch();+ } |]+ $ \batch ->+ reifyBatch (fromObject batch) n (const True) where fromObject :: JObject -> J x fromObject = unsafeCast instance (Interpretation a, BatchReflect a) => Reflect (V.Vector a) where- reflect v = do- _batch <- upcast <$> reflectBatch v- _batchReader <-- unsafeUngeneric <$> newBatchReader (Proxy :: Proxy a)- jv <- [java| {- $_batchReader.setBatch($_batch);- return $_batchReader.getSize();- } |] >>= newArray :: IO (J ('Array (Interp a)))- let _jvo = arrayUpcast jv- () <- [java| {- for(int i=0;i<$_jvo.length;i++)- $_jvo[i] = $_batchReader.get(i);- } |]- return jv+ reflect v =+ withLocalRef (upcast <$> reflectBatch v) $ \_batch ->+ withLocalRef (unsafeUngeneric <$> newBatchReader (Proxy :: Proxy a))+ $ \_batchReader -> do+ jv <- [java| {+ $_batchReader.setBatch($_batch);+ return $_batchReader.getSize();+ } |] >>= newArray :: IO (J ('Array (Interp a)))+ let _jvo = arrayUpcast jv+ () <- [java| {+ for(int i=0;i<$_jvo.length;i++)+ $_jvo[i] = $_batchReader.get(i);+ } |]+ return jv instance Batchable a => Batchable (V.Vector a) where type Batch (V.Vector a) = ArrayBatch (Batch a) instance BatchReify a => BatchReify (V.Vector a) where- newBatchWriter _ = do- _b <- unsafeUngeneric <$> newBatchWriter (Proxy :: Proxy a)- generic <$> [java| new BatchWriters.ObjectArrayBatchWriter($_b) |]+ newBatchWriter _ =+ withLocalRef+ (unsafeUngeneric <$> newBatchWriter (Proxy :: Proxy a))+ $ \_b ->+ generic <$> [java| new BatchWriters.ObjectArrayBatchWriter($_b) |] reifyBatch =- reifyArrayBatch (flip reifyBatch) (fmap (fmap return) . V.unsafeSlice)+ reifyArrayBatch+ -- skipped values do not leave holes in a batch of arrays, so it+ -- is safe to reify all of the values+ (\sz j -> reifyBatch j sz (const True))+ (fmap (fmap return) . V.unsafeSlice) instance BatchReflect a => BatchReflect (V.Vector a) where- newBatchReader _ = do- _b <- unsafeUngeneric <$> newBatchReader (Proxy :: Proxy a)- generic <$> [java| new BatchReaders.ObjectArrayBatchReader($_b) |]+ newBatchReader _ =+ withLocalRef+ (unsafeUngeneric <$> newBatchReader (Proxy :: Proxy a))+ $ \_b ->+ generic <$> [java| new BatchReaders.ObjectArrayBatchReader($_b) |] reflectBatch = reflectArrayBatch reflectBatch V.length (return . V.concat)++ instance Batchable a => Batchable (Nullable a) where+ type Batch (Nullable a) =+ 'Class "io.tweag.jvm.batching.Tuple2" <>+ '[ 'Array ('Prim "boolean")+ , ArrayBatch (Batch a)+ ]++ instance BatchReify a => BatchReify (Nullable a) where+ newBatchWriter _ =+ withLocalRef+ (unsafeUngeneric <$> newBatchWriter (Proxy :: Proxy a))+ $ \_baseBatcher ->+ generic <$> [java| new BatchWriters.NullableBatchWriter($_baseBatcher) |]++ reifyBatch jxs n p = do+ let _batch = unsafeUngeneric jxs+ isnull <- withLocalRef [java| $_batch._1 |] $ \j ->+ V.convert <$> (reify (unsafeCast (j :: JObject)) :: IO (VS.Vector W8Bool))+ let p' i = p i && isnull V.! i == 0+ b <- withLocalRef [java| $_batch._2 |] $ \j0 ->+ (\j -> reifyBatch j n p') (unsafeCast (j0 :: JObject) :: J (Batch a))+ return $ V.zipWith toNullable isnull b+ where+ toNullable :: W8Bool -> a -> Nullable a+ toNullable 0 a = NotNull a+ toNullable _ _ = Null |]
src/main/java/io/tweag/jvm/batching/BatchWriters.java view
@@ -341,6 +341,8 @@ private int[] end; // top tells the first position not occupied by arrays in the batch. private int top;+ // index of the next position available in the batch+ private int nextIndex; public ObjectArrayBatchWriter(BatchWriter<T, B> ob) { this.ob = ob;@@ -350,6 +352,7 @@ end = new int[size]; arrays = (T[][]) new Object[size][]; top = 0;+ nextIndex = 0; } public void set(int i, T[] vec) { if (vec == null)@@ -357,18 +360,21 @@ arrays[i] = vec; top += vec.length; end[i] = top;+ nextIndex = i + 1; } public Tuple2<B, int[]> getBatch() { ob.start(top); int k = 0;- for(int i=0;i<end.length && arrays[i]!=null;i++) {- for(int j=0;j<arrays[i].length;j++) {- ob.set(k, arrays[i][j]);- k++;+ for(int i=0;i<nextIndex;i++) {+ if (arrays[i] != null) {+ for(int j=0;j<arrays[i].length;j++) {+ ob.set(k, arrays[i][j]);+ k++;+ }+ // Release the reference to the array so it can be reclaimed+ // before the loop is over.+ arrays[i] = null; }- // Release the reference to the array so it can be reclaimed- // before the loop is over.- arrays[i] = null; } return new Tuple2<B, int[]>(ob.getBatch(), end); }@@ -404,6 +410,28 @@ arrays[i] = null; } return new Tuple2<char[], int[]>(batch, end);+ }+ }++ public static final class NullableBatchWriter<A, B>+ implements BatchWriter<A, Tuple2<boolean[], B>> {+ private boolean isnull[];+ final BatchWriter<A, B> b;+ public NullableBatchWriter(BatchWriter<A, B> b) {+ this.b = b;+ }+ public void start(int size) {+ b.start(size);+ isnull = new boolean[size];+ }+ public void set(int i, A a) {+ if (null == a)+ isnull[i] = true;+ else+ b.set(i, a);+ }+ public Tuple2<boolean[], B> getBatch() {+ return new Tuple2(isnull, b.getBatch()); } } }
src/test/haskell/Language/Java/BatchingSpec.hs view
@@ -12,6 +12,7 @@ import qualified Data.Vector.Storable as VS import Language.Java import Language.Java.Batching ()+import Language.Java.Inline import Test.Hspec spec :: Spec@@ -23,6 +24,24 @@ testrr (V.fromList [] :: V.Vector Text) it "succeeds on non-empty vectors" $ do testrr (V.fromList [1..10] :: V.Vector Int32)+ it "succeeds on vectors with nulls" $ do+ v <- [java| new Integer[] {1, null, 2, null } |]+ reify v `shouldReturn` (V.fromList [NotNull 1, Null, NotNull 2, Null] :: V.Vector (Nullable Int32))+ it "succeeds on vectors of vectors with nulls" $ do+ v <- [java| new Integer[][] {+ new Integer[] {},+ null,+ new Integer[] { 2, 3 },+ null,+ new Integer[] {}+ } |]+ reify v `shouldReturn` ((V.fromList+ [ NotNull V.empty+ , Null+ , NotNull (V.fromList [2, 3])+ , Null+ , NotNull V.empty+ ]) :: V.Vector (Nullable (V.Vector Int32))) it "succeeds on vectors of vectors" $ do let xs :: V.Vector (V.Vector Int32) xs = V.fromList (map V.fromList [ [i .. i + 4] | i <- [1,6..26] ])
src/test/haskell/Main.hs view
@@ -1,10 +1,18 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-} module Main where -import Language.Java (withJVM)+import Control.Concurrent (runInBoundThread)+import Foreign.JNI+import Language.Java.Inline (loadJavaWrappers) import qualified Spec import Test.Hspec main :: IO ()-main = withJVM ["-Djava.class.path=build/libs/jvm-batching.jar"] $- hspec Spec.spec+main = withJVM ["-Djava.class.path=build/libs/jvm-batching.jar"] $ do+ loadJavaWrappers -- causes classes in jvm-batching.jar to be loaded+ -- since they are used in java quotations+ hspec $ around_ (runInBoundThread . runInAttachedThread) Spec.spec