Programmers commonly use languages like Python, or C/C++ for those seeking higher performance, to handle array manipulation with libraries such as NumPy, SciPy, or deep learning frameworks like TensorFlow. Although Haskell is not a mainstream language, some developers favor its functional paradigm and strong typing.
Importantly, Haskell offers its own libraries for high-performance array manipulation. This article will introduce two of the most popular Haskell libraries for this purpose: Massiv and Accelerate.
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, or eDSL, for array-based computations designed for massive parallelism embedded within Haskell. In contrast, Massiv 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 or 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 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 the 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 and setup instructions are available in this GitHub repository. It includes setup instructions for both bare-metal and Docker environments.
The code was run with the following hardware specifications:
- Apple M5 Pro
- 24 GB RAM
- macOS Tahoe
Benchmarking CPU Throughput and Resource Utilization
We used Criterion for performance analysis. Running the comparison produced the following results:
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 | Delta or speedup |
|---|---|---|---|
| Mean execution time | 9.08 ms | 1.67 ms | Accelerate is approximately 5.4x faster |
| Best-run latency | 8.93 ms | 1.53 ms | Accelerate reaches a considerably lower latency |
| Execution consistency (R²) | 0.997, ultra-stable | 0.993, higher outlier variance | Massiv is more predictable |
After comparing their execution times, we can examine how the libraries differ in resource and CPU utilization.
Massiv run
To analyze resource utilization for Massiv, execute:
cabal run massiv-vs-accelerate -- stencil-massiv +RTS -s
Accelerate run
To analyze resource utilization for Accelerate, execute:
cabal run massiv-vs-accelerate -- stencil-accel +RTS -s
Memory and resource profiling comparison
The following results were collected on a 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 through the GHC Runtime System using +RTS -s, Accelerate requires significantly more resource backing to orchestrate its execution. Over the course of the benchmark suite, Accelerate generated 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 impressive language-level frugality. It peaked at just 309 MiB of active memory and generated only 43.95 GB of total allocations. This allowed its parallel worker loops to spend 98.1% of elapsed time executing pure data mutation with virtually no garbage collection interference.
Throughput and latency
Accelerate is the undisputed winner in raw speed. With a median time of approximately 1.46 ms compared to Massiv's approximately 8.91 ms, Accelerate achieved a 6.1x performance lead.
Why Accelerate performs this way: Accelerate leverages LLVM to generate SIMD, or Single Instruction, Multiple Data, instructions. It utilizes the AArch64 Neon units of bare metal, a Mac in our case, to process multiple pixels in a single clock cycle.
Why Massiv performs this way: 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 and 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 stayed out of the way, allowing the CPU to focus almost 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 an allocation rate of approximately 6.1 GB/s. While this is not leaked memory, it indicates a high volume of administrative traffic between the Haskell Runtime and 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 does not 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 suitable 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 can justify the additional 358 MiB of RAM.
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 into vector instructions on the metal.
Bonus: Docker vs. bare metal
Environment comparison matrix
| Metric | Docker container (virtualized Linux) | Bare-metal Apple M5 Pro (macOS Tahoe) | Bare-metal difference |
|---|---|---|---|
| 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 |
| Performance gap | Accelerate was 1.9x faster | Accelerate was 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 the container.
This suggests that GHC's native multithreaded worker loops for Massiv thrived under the Linux kernel's thread orchestration inside Docker. Running natively on macOS Tahoe, meanwhile, 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, GPU and CUDA offloading, and LLVM runtime compilation.
- Official website and 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: A breakdown of the runtime LLVM JIT engine.
2. Massiv (Data.Massiv.Array)
Focus: Native Haskell multicore 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 and saving image formats such as PNG, JPG, and TIFF directly into Massiv multidimensional arrays.
lehins/massiv-compat: Interoperability adapters for converting Massiv arrays to and from vector and Repa structures.





Top comments (0)