Choosing Between Lazy Lists, Unboxed Vectors, Sequences, and IntSet in Haskell for Performance‑Critical Pipelines
A decision guide comparing Haskell’s lazy list, unboxed vector, Data.Sequence, and IntSet for a large‑integer pipeline, with a concrete Criterion benchmark example.
15 Feb 2026, 10:36 UTC

Decision and constraints
You need a data structure for a pure Haskell pipeline that processes a large list of integers sequentially. The pipeline must keep memory usage low while maintaining low access latency, and it should interoperate with existing pure code without requiring monadic effects.
Supported options
| Option | Laziness | Memory overhead | Typical use case |
|---|---|---|---|
[Int] | Lazy | Low (pointers per element) | Simple traversal, incremental generation |
Data.Vector.Unboxed | Strict (contiguous) | Very low (packed primitive values) | Numeric loops where size is known or can be copied |
Data.Sequence | Strict (spine) | Moderate (balanced tree nodes) | Need logarithmic random access or efficient concatenation |
Data.IntSet | Strict (tree) | Low‑moderate (tree nodes) | Set operations such as membership, union, difference |
Trade‑offs
- Lazy lists avoid allocating the whole structure upfront, which can save memory when the list is generated on demand. However, each element is a pointer to a cons cell, causing indirection and potential space leaks if the list is retained.
- Unboxed vectors store elements in a contiguous block of primitive values, giving cache‑friendly access and minimal memory footprint. They require the element type to be primitive (e.g.,
Int,Word64) and either a known size upfront or a copying step to build the vector. - Data.Sequence provides a strict spine with logarithmic random access and efficient appends at both ends. The constant factor is higher than a vector due to tree node overhead, but it avoids the need to know the final size.
- Data.IntSet is optimized for set operations, offering
O(log n)membership, insert, and delete. It carries extra tree overhead compared to a plain vector and is only appropriate when set semantics are needed.
Concrete implementation and validation
The following snippet uses criterion to benchmark summing ten million Int values with each structure. It does not claim to have been run; it shows how you would set up the experiment.
-- Benchmark.hs
{-# LANGUAGE BangPatterns #-}
import Criterion.Main
import qualified Data.Vector.Unboxed as V
import qualified Data.Sequence as Seq
import qualified Data.IntSet as IntSet
-- generate a large list of Ints
largeList :: [Int]
largeList = [1..10_000_000]
-- convert to unboxed vector (requires copying)
largeVector :: V.Vector Int
largeVector = V.fromList largeList
-- convert to Sequence
largeSeq :: Seq.Seq Int
largeSeq = Seq.fromList largeList
-- convert to IntSet (note: duplicates are removed)
largeSet :: IntSet.IntSet
largeSet = IntSet.fromList largeList
sumList :: [Int] -> Int
sumList = foldl' (+) 0
sumVector :: V.Vector Int -> Int
sumVector v = V.foldl' (+) 0 v
sumSeq :: Seq.Seq Int -> Int
sumSeq s = Seq.foldl' (+) 0 s
sumSet :: IntSet.IntSet -> Int
sumSet s = IntSet.foldl' (+) 0 s
main :: IO ()
main = defaultMain
[ bench "list" $ whnf sumList largeList
, bench "vector" $ whnf sumVector largeVector
, bench "sequence" $ whnf sumSeq largeSeq
, bench "intset" $ whnf sumSet largeSet
]
To build and run the benchmark:
- Save the code to a file, e.g.,
Benchmark.hs. - Ensure you have GHC 9.8.2 or later and the
criterionpackage installed (cabal update && cabal install criterionor viastack). - Run the following command with normal user privileges:
ghc -O2 Benchmark.hs -o benchmark
./benchmark
Expected checks:
- The terminal output will show a table of mean execution times and allocated memory for each benchmark.
- Look for the "vector" entry to have a lower mean time and significantly lower allocated bytes compared to the "list" entry – roughly a 2× speedup and <10% of the memory usage, as described in the findings.
- If you rebuild with
-O0(no optimizations), you should see the lazy list overhead increase, illustrating the impact of GHC’s optimizations on pointer indirection.
Limitations and practical verification
Benchmark results depend on the GHC version, optimization flags (-O2 used above), and the underlying CPU architecture. To obtain comparable numbers, repeat the experiment with the same compiler version and flags on the target hardware.
Unboxed vectors require the element type to be primitive. For non‑primitive data you must either define a suitable Unbox instance or fall back to boxed vectors, which will change both memory usage and performance characteristics.
A practical way to confirm the outcome is to inspect the generated benchmark executable’s output and verify that the vector’s allocated memory column is markedly smaller than that of the list, while the time column shows a noticeable reduction.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.