Choosing Between Data.Vector and Data.Vector.Mutable for High‑Performance Haskell Workloads
When performance matters, deciding between immutable and mutable vectors can change your program’s speed and memory usage. This guide explains the constraints, compares both options, and shows a concrete ST‑monad implementation that stays pure from the caller’s view.
16 Jan 2026, 01:43 UTC

Problem Statement
\nNumeric kernels in Haskell often need to update large dense arrays in a tight loop. The vector package offers two flavors: immutable Data.Vector and mutable Data.Vector.Mutable. Choosing the wrong one can lead to unnecessary allocations or subtle bugs. The decision must satisfy the following constraints:
- \n
- Compile with GHC 9.x and
vector≥ 0.12. \n - Expose a pure API to callers; the stateful part must be hidden inside
runST. \n - The algorithm must not escape the
STmonad. \n - Thread safety is required; the vector must not be shared unsafely between threads. \n
Given these constraints, the decision boils down to whether in‑place updates are worth the added complexity.
\nDecision & Constraints
\nDecision: Use Data.Vector.Mutable inside the ST monad for large, dense numeric updates, and return an immutable vector via runST. For simpler code or when updates are infrequent, stay with Data.Vector.
Constraints Recap:
\n- \n
- GHC 9.x, vector ≥ 0.12. \n
- Pure API surface. \n
- No escaping
STstate. \n - Thread‑safe usage. \n
Compact Comparison Table
\n| Aspect | Data.Vector | Data.Vector.Mutable (ST) |
|---|---|---|
| Update Cost | O(n) (copy on write) | O(1) (in‑place) |
| Memory Allocation | Large, frequent allocations | Minimal, single allocation |
| Purity | Pure | Pure via runST |
| Thread Safety | Safe to share | Unsafe to share; requires copy or sync |
| Debugging | Easy (no side‑effects) | Harder (stateful) |
| Typical Use | Small transforms, functional pipelines | Large loops, numeric kernels |
Trade‑Offs Explained
\n- \n
- Speed vs Simplicity: Mutable vectors give a ~2‑3× speedup for million‑element loops, but require careful scoping. Immutable vectors keep code simple but may double memory traffic. \n
- Safety: Mutable vectors bypass bounds checks for speed; an out‑of‑bounds write is undefined. Use
unsafeWriteonly when the index is guaranteed valid. \n - Concurrency: Immutable vectors can be freely shared across threads. Mutable vectors must be cloned or protected with
MVarorSTMif shared. \n - Garbage Collection: Mutable vectors reduce GC pressure because fewer intermediate arrays exist. \n
Concrete Implementation: In‑Place Factorial Accumulator
\nThe following example builds a factorial sequence in a mutable vector and then returns an immutable vector. The code stays pure because runST hides the mutable state.
{-# LANGUAGE RankNTypes #-}\nimport qualified Data.Vector as V\nimport qualified Data.Vector.Mutable as MV\nimport Control.Monad.ST (ST, runST)\n\n-- | Compute factorials up to n (inclusive) in a mutable vector.\nfactorialVec :: Int -> V.Vector Integer\nfactorialVec n = runST $ do\n mv :: MV.MVector s Integer <- MV.new (n + 1)\n MV.write mv 0 1 -- 0! = 1\n let loop i acc\n | i > n = return ()\n | otherwise = do\n MV.write mv i acc\n loop (i + 1) (acc * fromIntegral i)\n loop 1 1\n V.freeze mv\n\nKey points:
\n- \n
MV.writeis O(1); no bounds checks are performed at runtime for speed. \n - The entire mutable state lives inside the
STmonad;runSTguarantees it cannot escape. \n - The result is an immutable
Vectorsuitable for any pure consumer. \n
Validation Steps
\n- \n
- Compile with GHC 9.6 and flags
-Wall -Werrorto ensure no warnings. \n - Run
ghc factorial.hs -O2and execute the binary; verify the vector contains the expected factorials. \n - Use
Criterionto benchmark the loop against an immutable‑vector version that copies on each update. \n
Benchmark Skeleton with Criterion
\nimport Criterion.Main\nimport qualified Data.Vector as V\n\nmain :: IO ()\nmain = defaultMain [\n bench \"mutable factorial\" $ nf factorialVec 1000000,\n bench \"immutable loop\" $ nf immutableLoop 1000000\n ]\n\nimmutableLoop :: Int -> V.Vector Integer\nimmutableLoop n = foldl' (\\vec i -> vec V.// [(i, vec V.! (i-1) * fromIntegral i)]) (V.replicate (n+1) 1) [1..n]\n\nRun cabal bench or stack bench to see the expected ~2× speedup for the mutable version.
Practical Checks & Limitations
\n- \n
- Use
MV.unsafeWriteonly when you can statically prove the index is within bounds. \n - In multi‑threaded contexts, pass the immutable result or clone the mutable vector before sharing. \n
- Verify that no
runSTleaks by inspecting the generated Core withghc -ddump-core; theSTmonad should be inlined. \n - When debugging, prefer the immutable path; swap to mutable only when profiling shows a bottleneck. \n
Conclusion
\nFor performance‑critical numeric workloads that require many in‑place updates, Data.Vector.Mutable inside ST is the right choice. It offers lower allocation, faster updates, and minimal GC overhead. However, it demands careful scoping, bounds safety, and thread‑safety considerations. When these constraints are not critical, the simpler immutable vector remains a safe and expressive alternative.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.