Architectural Philosophies: Massiv vs. Accelerate
Massiv
Massiv, whose name comes from the Russian word for array, is a high-performance library for array manipulation. Unlike other libraries that treat arrays as objects in memory, Massiv uses the concept of "Delayed Arrays." These delayed arrays are represented not as objects but as functions or a composition of functions.
For instance, a delayed array can be defined as a function that maps an index (ix) to an element (e), with the type signature (ix -> e). The library provides numerous array types, along with the necessary type families for describing array dimensions and the required functions.
To achieve performance in array manipulation, Massiv utilizes stencil computations. This technique involves manipulating an array based on a fixed pattern, applying a computation within the neighborhood of the stencil's center (which is the zero index in Massiv). Massiv's documentation suggests that implementing this stencil computation in a functional way is more efficient.
Accelerate
Accelerate positions itself as a domain-specific language (eDSL) for array-based computations, designed for massive parallelism embedded within Haskell, in contrast to Massiv, which is a library. Accelerate programs are defined using parameterised collective operations. Accelerate then takes these computations, compiling and optimizing them specifically for the target architecture.
Accelerate uses the Array as its primary computational unit. Computations are executed as collective operations across arrays of the type Array sh e, where sh represents the array's shape (dimension) and e is the type of the elements. All Accelerate programs receive zero or more arrays as input and produce one or more arrays as output. The array elements are stored in a memory layout—specifically an unboxed, unzipped struct-of-array representation—that allows them to be efficiently transferred to and utilized by various devices, such as a GPU.
For more information, you can read the definition for Accelerate Arrays, which has a good explanation of how they are defined and used.
Code implementation of multidimensional array processing
To compare the performance of both libraries, I have implemented a box blur, a process that applies a filter to blur an image. The specific logic involved applying a 3x3 average filter to a 3162x3162 image. Here are implementation details:
runStencilBenchmarks :: IO ()
runStencilBenchmarks = do
let side = 3162
let sz = M.Sz2 side side
-- Massiv Setup
let mArr = M.makeArray M.Par sz (\(i M.:. j) -> fromIntegral (i + j)) :: M.Array M.P M.Ix2 Double
-- Accelerate Setup
let aArr = A.fromList (A.Z A.:. side A.:. side) [0..(fromIntegral (side*side - 1))] :: A.Array A.DIM2 Double
defaultMain [
bgroup "3x3 Mean Blur" [
bench "Massiv (Stencil)" $
nf (M.computeAs M.P . M.mapStencil (M.Fill 0.0) massivMeanStencil) mArr,
bench "Accelerate (LLVM Stencil)" $
whnf (Native.run . A.stencil accelMeanStencil A.clamp . A.use) aArr
]
]
The code implementation, along with the instructions to set up, are available here on this repository (Has set up instructions for both Bare Metal and Docker). Also the code was run the following hardware specifications:
- Apple M5 Pro
- 24 GB RAM
- macOS Tahoe
Benchmarking CPU Throughput and Resource Utilization
We use criterion for performance analysis, running the comparison we get the following results:
[IMAGE_1]
Whoa. Those results are staggering.
The performance disparity is stark: we observed a 5.4x difference. Massiv required approximately 9.08 ms, whereas Accelerate finished in about 1.67 ms. This clear gap underscores the value of these analyses though theoretically equivalent, Accelerate's specialized runtime execution engine significantly outperforms Massiv's native compiled loops in practice when dealing with parallel grid operations.
The results can be summarized in the following table
| Metric | Massiv (GHC Native + LLVM) | Accelerate | The Delta / Speedup | |
|---|---|---|---|---|
| Mean Execution Time | 9.08 ms | 1.67 ms | Accelerate is ~5.4x faster | |
| Best Run Latency | 8.93 ms | 1.53 ms | Accelerate hits a blistering low | |
| Execution Consistency (R²) | 0.997 (Ultra Stable) | 0.993 (High Outlier Variance) | Massiv is more predictable |
Now we just compare time between them, now let’s make a comparison between both but for resources and other CPU stuff.
Massiv run To analyze the resource utilization for Massiv, execute:
cabal run massiv-vs-accelerate -- stencil-massiv +RTS -s
[IMAGE_2]
cabal run massiv-vs-accelerate -- stencil-accel +RTS -s
[IMAGE_3]
📊 Memory & Resource Profiling Comparison (Bare-Metal M5 Pro).
| Metric | Massiv | Accelerate | Structural Difference |
|---|---|---|---|
| Total Allocated in Heap | 43.95 GB | 259.25 GB | Accelerate allocates 5.9x more metadata |
| Peak Memory In Use | 309 MiB | 667 MiB | Massiv is 2.1x more lightweight |
| Garbage Collections | 617 collections | 3,950 collections | Accelerate triggers 6.4x more GC runs |
| Total GC Time (Elapsed) | 0.084 seconds | 0.510 seconds | Accelerate spends 6x longer in GC pauses |
| Elapsed Productivity | 98.1% | 90.6% | Massiv keeps the CPUs focused on mutation |
The results reveal a classic engineering trade-off: Accelerate provides specialized hardware acceleration at the cost of higher memory overhead and runtime setup metadata, while Massiv provides seamless language integration and strict resource efficiency at the cost of raw throughput. Looking under the hood via the GHC Runtime System (`+RTS -s`), Accelerate requires significantly more resource backing to orchestrate its execution. Over the course of the benchmark suite, Accelerate generated a massive 259.25 GB of temporary heap allocations and peaked at 667 MiB of total memory in use, forcing GHC to trigger nearly 4,000 garbage collection cycles. Massiv, by contrast, showcases incredible language-level frugality: it peaked at just 309 MiB of active memory and generated only 43.95 GB of total allocations, allowing its parallel worker loops to spend a rock-solid 98.1% of elapsed time executing pure data mutation with virtually no garbage collection interference.
Throughput & Latency
Accelerate is the undisputed winner in raw speed. With a median time of ~1.46 ms compared to Massiv’s ~8.91 ms, Accelerate achieved a 6.1x performance lead.
- The "Why": Accelerate leverages LLVM to generate SIMD (Single Instruction, Multiple Data) instructions. It utilizes the AArch64 Neon units of bare metal (Mac in our case) to process multiple pixels in a single clock cycle.
- The "Why" for Massiv: Massiv relies on GHC’s optimization. While GHC is excellent at loop fusion, it is historically conservative with vectorization, resulting in "Scalar" execution that processes pixels one by one.
Memory & Resource Management
Massiv is the winner in system "politeness" and footprint.
- Heap Stability: Massiv achieved a near-perfect 98.1% elapsed productivity score. This means the GHC Garbage Collector (GC) stayed out of the way, allowing the CPU to focus entirely on math.
- System Tax: Massiv’s total memory footprint was only 309 MiB, compared to Accelerate’s 667 MiB. Accelerate’s higher RAM usage is the "cost of entry" for the LLVM JIT engine and its native memory buffers.
- Allocation Intensity: Accelerate showed a staggering allocation rate of ~6.1 GB/s. While this isn't "leaked" memory, it indicates a high volume of administrative traffic between the Haskell Runtime and the Native LLVM code.
Final Thoughts
When to choose Massiv
- Predictability and Ease of Deployment: Choose Massiv if your priority is predictability and ease of deployment.
- Pure Haskell ecosystem: It behaves like "Pure Haskell" at runtime, meaning your production environment doesn't need to manage a dynamic runtime JIT compilation layer.
- Complex Logic Interleaving: It excels in complex algorithms where you need to interleave standard Haskell logic with array processing.
- Remarkable Memory Efficiency: It is remarkably memory-efficient, peaking at just 309 MiB and keeping your application ideal for environments with strict RAM limits.
When to choose Accelerate
- Computational Throughput: Choose Accelerate if your priority is computational throughput.
- Heavy Latency-Critical Tasks: If you are processing high-resolution video frames, large-scale signals, or heavy matrices where every millisecond counts, the 6.1x speedup heavily justifies the extra 358 MiB of RAM footprint.
- Compiling to the Metal: It is the closest Haskell gets to writing low-level performance code, providing a high-level embedded DSL that compiles directly down to vector instructions on the metal.
Bonus: Comparison between Docker vs Bare Metal (Criterion Only).
[IMAGE_4]
[IMAGE_5]
Environment Comparison Matrix (Docker vs. Bare-Metal)
| Metric | Docker Container (Virtualized Linux) | Bare-Metal Apple M5 Pro (macOS Tahoe) | The Bare-Metal Edge |
|---|---|---|---|
| Massiv Stencil | 3.39 ms | 8.93 ms | 2.63x slower on bare metal |
| Accelerate Stencil | 1.78 ms | 1.53 ms | 1.16x faster on bare metal |
| The Performance Gap | Accelerate was 1.9x faster | Accelerate is 5.8x faster | Native environment favors JIT pipelines |
Comparing these environments reveals a fascinating divergence: while Accelerate achieves a modest speedup on bare metal by freeing its JIT compiler from hypervisor overhead, Massiv actually performs slower outside of the container. This suggests that GHC’s native multi-threaded worker loops for Massiv thrived under the Linux kernel's thread orchestration inside Docker, whereas running natively on macOS Tahoe heavily widens the performance gap in favor of Accelerate's hardware-mapped vector units.
Related resources:
To help you gain a deeper understanding of both libraries, here is a list of related resources:
1. Accelerate (Data.Array.Accelerate)
Focus: Deeply embedded language (eDSL), GPU (CUDA) offloading, and LLVM runtime compilation.
- Official Website & User Guide: acceleratehs.org
- GitHub Repository: AccelerateHS/accelerate
- Hackage Documentation: accelerate on Hackage
Academic Papers: - "Accelerating Haskell Array Codes with Multicore GPUs": The seminal paper detailing how embedded ASTs are compiled to CUDA on the fly.
- "Type-safe Runtime Code Generation: Accelerate to LLVM": Breakdown of the runtime LLVM JIT engine.
2. Massiv (Data.Massiv.Array)
Focus: Native Haskell multi-core CPU evaluation, explicit indexing, stencil computation, and work-stealing schedulers.
- GitHub Repository: lehins/massiv
- Hackage Documentation: massiv on Hackage
- Massiv Ecosystem Extras:
- massiv-io: High-performance I/O for loading/saving image formats (PNG, JPG, TIFF) directly into massiv multidimensional arrays.
- lehins/massiv-compat: Interoperability adapters converting massiv arrays to/from vector and repa structures.