Haskell
performance optimization
vectors
functional programming
software development

Using vectors for performance improvement in Haskell

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Haskell lists are elegant, but they are not the best structure for every performance-sensitive task. When you need fast indexing, better cache locality, or dense numeric storage, vectors are often a much better choice. The vector package gives you both immutable and mutable options while still fitting naturally into idiomatic Haskell.

Why Vectors Are Faster Than Lists in Many Cases

Lists are linked structures. That makes prepending cheap and recursion pleasant, but it also means indexing is linear and memory layout is scattered. A vector stores elements contiguously, which improves cache behavior and makes random access effectively constant time.

That matters for workloads such as:

  • numeric loops
  • dynamic programming tables
  • repeated indexing into large datasets
  • algorithms that traverse arrays many times

If your code frequently uses !! on a list, you are usually looking at an opportunity to switch to vectors.

Immutable Vectors for Dense Data

For read-mostly data, immutable vectors are the easiest starting point.

haskell
1import qualified Data.Vector.Unboxed as U
2
3dotProduct :: U.Vector Double -> U.Vector Double -> Double
4dotProduct xs ys = U.sum (U.zipWith (*) xs ys)
5
6main :: IO ()
7main = do
8  let a = U.fromList [1.0, 2.0, 3.0]
9      b = U.fromList [4.0, 5.0, 6.0]
10  print (dotProduct a b)

This example uses Data.Vector.Unboxed, which is ideal for primitive numeric types. Unboxed vectors avoid pointer indirection and store raw values directly, which usually improves both speed and memory use.

Mutable Vectors for In-Place Updates

When an algorithm needs many updates, a mutable vector inside ST or IO can avoid repeated allocation of new immutable structures.

haskell
1import qualified Data.Vector as V
2import qualified Data.Vector.Mutable as MV
3import Control.Monad
4import Control.Monad.ST
5
6bubblePass :: V.Vector Int -> V.Vector Int
7bubblePass input = runST $ do
8  mv <- V.thaw input
9  let lastIndex = MV.length mv - 1
10  forM_ [0 .. lastIndex - 1] $ \i -> do
11    left <- MV.read mv i
12    right <- MV.read mv (i + 1)
13    when (left > right) $ do
14      MV.write mv i right
15      MV.write mv (i + 1) left
16  V.freeze mv

The mutable work stays local, and the outside API remains pure. That is a common Haskell pattern when performance matters but you still want clean functional boundaries.

Boxed, Unboxed, and Storable Choices

Choosing the right vector type matters:

  • 'Data.Vector is boxed and works for any type'
  • 'Data.Vector.Unboxed is usually best for primitive values such as Int or Double'
  • 'Data.Vector.Storable is useful when you need C-compatible memory layout'

If you use boxed vectors for numeric code by default, you may leave performance on the table. The unboxed version is often the real win in data-heavy code.

Fusion and How to Keep It Working

The vector package supports fusion, which can combine chained operations and avoid intermediate allocations. Code such as U.sum (U.map f vec) can compile into a tight loop instead of building a whole mapped vector first.

Fusion is powerful, but it is easier to lose than many people realize. Converting back and forth to lists, forcing unnecessary intermediate structures, or mixing APIs carelessly can stop the optimizer from helping you.

Common Pitfalls

The most common mistake is switching from lists to boxed vectors and expecting dramatic numeric speedups automatically. For numbers, unboxed vectors are often the more meaningful upgrade.

Another issue is overusing indexing in a way that still recreates vectors repeatedly. Vectors give you cheap reads, but immutable updates still allocate unless you switch to a mutable workflow inside ST or IO.

Be careful when benchmarking. Tiny examples can hide allocation behavior, fusion effects, and garbage collection costs. Use realistic inputs and benchmark the whole operation, not just one expression in isolation.

Finally, do not replace lists blindly. If your code mainly streams data once from left to right, a list or streaming library may still be the better abstraction. Vectors help most when the access pattern benefits from array semantics.

Summary

  • Vectors improve indexing speed and memory locality compared with lists.
  • Use immutable vectors for read-heavy code and mutable vectors for update-heavy inner loops.
  • Prefer unboxed vectors for primitive numeric data when possible.
  • Let fusion help you by keeping operations in the vector world.
  • Choose vectors when your algorithm needs array-like access, not just because they sound faster.

Course illustration
Course illustration

All Rights Reserved.