Đã 7 năm trôi qua kể từ khi câu hỏi này được đặt ra, và dường như vẫn chưa có ai nghĩ ra giải pháp tốt cho vấn đề này. Repa không có chức năng mapM
/ traverse
like, thậm chí có thể chạy mà không cần song song hóa. Hơn nữa, xem xét mức độ tiến bộ đã có trong vài năm qua, có vẻ như điều đó cũng khó xảy ra.
Do tình trạng cũ kỹ của nhiều thư viện mảng trong Haskell và sự không hài lòng chung của tôi với các bộ tính năng của chúng, tôi đã đặt một vài năm làm việc vào thư viện mảng massiv
, nó mượn một số khái niệm từ Repa, nhưng đưa nó lên một cấp độ hoàn toàn khác. Quá đủ với phần giới thiệu.
Trước khi đến ngày hôm nay, đã có ba bản đồ monadic như chức năng trong massiv
(không kể các từ đồng nghĩa như chức năng: imapM
, forM
et al.):
mapM
- ánh xạ thông thường trong một tùy ý Monad
. Không thể song song hóa vì những lý do rõ ràng và cũng hơi chậm (dọc theo dòng thông thường mapM
trên danh sách chậm)
traversePrim
- ở đây chúng tôi bị hạn chế PrimMonad
, nhanh hơn đáng kể mapM
, nhưng lý do cho điều này không quan trọng đối với cuộc thảo luận này.
mapIO
- điều này, như tên cho thấy, bị hạn chế IO
(hoặc đúng hơn MonadUnliftIO
, nhưng điều đó không liên quan). Bởi vì chúng tôi đang ở trong, IO
chúng tôi có thể tự động chia mảng thành nhiều phần khi có lõi và sử dụng các luồng công nhân riêng biệt để ánh xạ IO
hành động trên từng phần tử trong các phần đó. Không giống như thuần túy fmap
, cũng có thể song song hóa, chúng tôi phải ở IO
đây vì tính không xác định của lập lịch kết hợp với các tác dụng phụ của hành động lập bản đồ của chúng tôi.
Vì vậy, khi tôi đọc câu hỏi này, tôi tự nghĩ rằng vấn đề thực tế đã được giải quyết trong đó massiv
, nhưng không quá nhanh. Các trình tạo số ngẫu nhiên, chẳng hạn như in mwc-random
và các trình tạo số khác random-fu
không thể sử dụng cùng một trình tạo số trên nhiều luồng. Có nghĩa là, mảnh ghép duy nhất mà tôi còn thiếu là: "vẽ một hạt ngẫu nhiên mới cho mỗi sợi được sinh ra và tiến hành như bình thường". Nói cách khác, tôi cần hai thứ:
- Một chức năng sẽ khởi tạo càng nhiều trình tạo càng có nhiều luồng công nhân
- và một phần trừu tượng sẽ liên tục cung cấp trình tạo chính xác cho chức năng ánh xạ tùy thuộc vào luồng mà hành động đang chạy.
Vì vậy, đó chính xác là những gì tôi đã làm.
Trước tiên, tôi sẽ đưa ra các ví dụ bằng cách sử dụng các hàm randomArrayWS
và được chế tạo đặc biệt initWorkerStates
, vì chúng phù hợp hơn với câu hỏi và sau đó chuyển sang bản đồ đơn nguyên tổng quát hơn. Đây là loại chữ ký của họ:
randomArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates g -- ^ Use `initWorkerStates` to initialize you per thread generators
-> Sz ix -- ^ Resulting size of the array
-> (g -> m e) -- ^ Generate the value using the per thread generator.
-> m (Array r ix e)
initWorkerStates :: MonadIO m => Comp -> (WorkerId -> m s) -> m (WorkerStates s)
Đối với những người không quen thuộc massiv
, Comp
đối số là một chiến lược tính toán để sử dụng, các hàm tạo đáng chú ý là:
Seq
- chạy tính toán một cách tuần tự, không có bất kỳ luồng nào
Par
- tạo ra càng nhiều chủ đề càng tốt và sử dụng chúng để thực hiện công việc.
Tôi sẽ sử dụng mwc-random
gói làm ví dụ ban đầu và sau đó chuyển sang RVarT
:
λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)
Ở trên, chúng tôi đã khởi tạo một trình tạo riêng cho mỗi luồng bằng cách sử dụng tính ngẫu nhiên của hệ thống, nhưng chúng tôi cũng có thể sử dụng một trình tạo duy nhất cho mỗi hạt luồng bằng cách lấy nó từ WorkerId
đối số, chỉ là một chỉ Int
mục của worker. Và bây giờ chúng ta có thể sử dụng các trình tạo đó để tạo một mảng với các giá trị ngẫu nhiên:
λ> 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 ]
]
Bằng cách sử dụng Par
chiến lược, scheduler
thư viện sẽ chia đều công việc tạo ra giữa các công nhân có sẵn và mỗi công nhân sẽ sử dụng bộ tạo riêng của nó, do đó làm cho nó an toàn. Không có gì ngăn cản chúng tôi sử dụng lại cùng WorkerStates
một số lần tùy ý miễn là nó không được thực hiện đồng thời, nếu không sẽ dẫn đến ngoại lệ:
λ> 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 ]
Bây giờ đặt mwc-random
sang một bên, chúng ta có thể sử dụng lại khái niệm tương tự cho các trường hợp sử dụng khả thi khác bằng cách sử dụng các hàm như generateArrayWS
:
generateArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> Sz ix -- ^ size of new array
-> (ix -> s -> m e) -- ^ element generating action
-> m (Array r ix e)
và mapWS
:
mapWS ::
(Source r' ix a, Mutable r ix b, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> (a -> s -> m b) -- ^ Mapping action
-> Array r' ix a -- ^ Source array
-> m (Array r ix b)
Dưới đây là ví dụ hứa về cách sử dụng chức năng này với rvar
, random-fu
và mersenne-random-pure64
thư viện. Chúng tôi cũng có thể sử dụng randomArrayWS
ở đây, nhưng vì lợi ích của ví dụ, giả sử chúng tôi đã có một mảng với các RVarT
s khác nhau , trong trường hợp đó chúng tôi cần một 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 ]
]
Điều quan trọng cần lưu ý là mặc dù thực tế là việc triển khai thuần túy Mersenne Twister đang được sử dụng trong ví dụ trên, chúng ta không thể thoát khỏi IO. Điều này là do lập lịch không xác định, có nghĩa là chúng ta không bao giờ biết được nhân viên nào sẽ xử lý phần nào của mảng và do đó trình tạo nào sẽ được sử dụng cho phần nào của mảng. Ở phía trên, nếu trình tạo thuần túy và có thể phân tách, chẳng hạn như splitmix
, thì chúng ta có thể sử dụng hàm tạo thuần túy, xác định và có thể song song hóa:, randomArray
nhưng đó đã là một câu chuyện riêng.