Stack Builders logo
AlexanderCoronel
Alexander Coronel
Aug. 17, 2026
Aug. 17, 2026
9 min read
Subscribe to blog
Email
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

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.

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.
Subscribe to blog
Email