Bu sorunun sorulmasının üzerinden 7 yıl geçti ve hala kimse bu soruna iyi bir çözüm bulamadı gibi görünüyor. Repa'nın paralelleştirme olmadan çalışabilen bir mapM
/ traverse
benzeri işlevi yoktur. Dahası, son birkaç yılda kaydedilen ilerleme miktarı düşünüldüğünde, bunun da gerçekleşmesi olası görünmüyor.
Haskell'deki birçok dizi kütüphanesinin bayat durumu ve özellik setlerinden genel olarak memnuniyetsizliğim nedeniyle massiv
, Repa'dan bazı kavramları ödünç alan, ancak onu tamamen farklı bir düzeye taşıyan bir dizi kitaplığına birkaç yıllık çalışmayı koydum . Giriş için bu kadar yeter.
Bugünden önce, içinde üç monadik harita benzeri işlev vardı massiv
(işlevler gibi eşanlamlıları saymazsak:, imapM
vb forM
.):
mapM
- rasgele olağan haritalama Monad
. Bariz nedenlerden dolayı paralelleştirilemez ve aynı zamanda biraz yavaştır ( mapM
bir liste üzerinde olağan çizgileri boyunca yavaş)
traversePrim
- burada PrimMonad
önemli ölçüde daha hızlı olan ile sınırlıyız mapM
, ancak bunun nedeni bu tartışma için önemli değil.
mapIO
- bu, adından da anlaşılacağı gibi, sınırlıdır IO
(veya daha doğrusu MonadUnliftIO
, ancak bu alakasızdır). İçinde bulunduğumuz için, IO
diziyi çekirdekler olduğu kadar çok sayıda parçaya otomatik olarak bölebiliriz ve IO
bu parçalardaki her bir öğe üzerindeki eylemi eşlemek için ayrı çalışan iş parçacıkları kullanabiliriz . fmap
Aynı zamanda paralelleştirilebilir olan saftan farklı IO
olarak, haritalama eylemimizin yan etkileri ile birlikte çizelgelemenin determinizminin olmaması nedeniyle burada olmak zorundayız .
Bu soruyu okuduktan sonra, kendi kendime sorunun pratikte çözüldüğünü düşündüm massiv
, ama o kadar hızlı değil. İçinde mwc-random
ve diğerleri gibi rastgele sayı üreteçleri, random-fu
birçok iş parçacığında aynı oluşturucuyu kullanamaz. Bunun anlamı, bulmacanın tek eksik parçasının: "ortaya çıkan her iş parçacığı için yeni bir rastgele tohum çizmek ve her zamanki gibi ilerlemek" idi. Başka bir deyişle, iki şeye ihtiyacım vardı:
- Çalışan iş parçacığı olacak kadar üreteci başlatan bir işlev
- ve eylemin hangi iş parçacığında çalıştığına bağlı olarak eşleme işlevine sorunsuz bir şekilde doğru üreteci verecek bir soyutlama.
Ben de aynen öyle yaptım.
Öncelikle soruyla daha alakalı oldukları ve daha sonra daha genel monadik haritaya geçtikleri için özel olarak hazırlanmış randomArrayWS
ve initWorkerStates
işlevleri kullanarak örnekler vereceğim . İşte tip imzaları:
randomArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates g
-> Sz ix
-> (g -> m e)
-> m (Array r ix e)
initWorkerStates :: MonadIO m => Comp -> (WorkerId -> m s) -> m (WorkerStates s)
Aşina olmayanlar massiv
için Comp
argüman, kullanılacak bir hesaplama stratejisidir, dikkate değer kurucular şunlardır:
Seq
- herhangi bir iş parçacığını çatallamadan hesaplamayı sırayla çalıştırın
Par
- Yetenekleriniz kadar iş parçacığı açın ve işi yapmak için bunları kullanın.
mwc-random
Paketi başlangıçta örnek olarak kullanacağım ve daha sonra şu adrese geçeceğim RVarT
:
λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)
Yukarıda, sistem rasgeleliğini kullanarak iş parçacığı başına ayrı bir oluşturucu başlattık, ancak iş parçacığının WorkerId
salt bir Int
indeksi olan argümandan türeterek iş parçacığı başına benzersiz bir tohum da kullanabilirdik . Ve şimdi bu üreteçleri rastgele değerlerle bir dizi oluşturmak için kullanabiliriz:
λ> randomArrayWS gens (Sz2 2 3) standard :: IO (Array P Ix2 Double)
Array P Par (Sz (2 :. 3))
[ [ -0.9066144845415213, 0.5264323240310042, -1.320943607597422 ]
, [ -0.6837929005619592, -0.3041255565826211, 6.53353089112833e-2 ]
]
Par
Strateji kullanarak , scheduler
kütüphane üretim işini mevcut işçiler arasında eşit bir şekilde paylaştıracak ve her işçi kendi jeneratörünü kullanacak ve böylece iş parçacığını güvenli hale getirecektir. WorkerStates
Eşzamanlı olarak yapılmadığı sürece, hiçbir şey aynı keyfi sayıda tekrar kullanmamızı engellemez , aksi takdirde bir istisna ile sonuçlanır:
λ> randomArrayWS gens (Sz1 10) (uniformR (0, 9)) :: IO (Array P Ix1 Int)
Array P Par (Sz1 10)
[ 3, 6, 1, 2, 1, 7, 6, 0, 8, 8 ]
Şimdi bir mwc-random
kenara koyarsak, aynı kavramı diğer olası kullanım durumları için aşağıdaki gibi işlevleri kullanarak yeniden kullanabiliriz generateArrayWS
:
generateArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> Sz ix
-> (ix -> s -> m e)
-> m (Array r ix e)
ve mapWS
:
mapWS ::
(Source r' ix a, Mutable r ix b, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> (a -> s -> m b)
-> Array r' ix a
-> m (Array r ix b)
İşte bununla işlevselliği nasıl kullanılacağı konusunda söz örnektir rvar
, random-fu
ve mersenne-random-pure64
kütüphaneler. Burada da kullanabilirdik randomArrayWS
, ancak örnek olarak, farklı s'lere sahip bir dizimiz olduğunu RVarT
varsayalım, bu durumda a mapWS
:
λ> import Data.Massiv.Array
λ> import Control.Scheduler (WorkerId(..), initWorkerStates)
λ> import Data.IORef
λ> import System.Random.Mersenne.Pure64 as MT
λ> import Data.RVar as RVar
λ> import Data.Random as Fu
λ> rvarArray = makeArrayR D Par (Sz2 3 9) (\ (i :. j) -> Fu.uniformT i j)
λ> mtState <- initWorkerStates Par (newIORef . MT.pureMT . fromIntegral . getWorkerId)
λ> mapWS mtState RVar.runRVarT rvarArray :: IO (Array P Ix2 Int)
Array P Par (Sz (3 :. 9))
[ [ 0, 1, 2, 2, 2, 4, 5, 0, 3 ]
, [ 1, 1, 1, 2, 3, 2, 6, 6, 2 ]
, [ 0, 1, 2, 3, 4, 4, 6, 7, 7 ]
]
Yukarıdaki örnekte saf Mersenne Twister uygulamasının kullanılmasına rağmen, IO'dan kaçamayacağımızı belirtmek önemlidir. Bunun nedeni, deterministik olmayan zamanlamadan kaynaklanmaktadır; bu, hangi çalışanların dizinin hangi parçasını işleyeceğini ve dolayısıyla dizinin hangi bölümü için hangi üretecin kullanılacağını asla bilemeyeceğimiz anlamına gelir. Üst tarafta, eğer jeneratör saf ve bölünebilir ise, örneğin splitmix
, o zaman saf, deterministik ve paralelleştirilebilir üretim fonksiyonunu kullanabiliriz: randomArray
ama bu zaten ayrı bir hikaye.