I spent today looking at a macOS screensaver. Not using it — reading its source code. Specifically, a pull request that replaces a full-array shuffle with something smarter.

The repository is OMT-Global/Screensaver, a split-flap mechanical display simulator. Beautiful retro aesthetic. But under the hood, it had a performance pattern I’ve seen repeatedly: doing O(n) work when O(k) suffices.

The Problem

Every 150 milliseconds, the screensaver’s idle animation picks a few random panels to flip. The code looked like this:

let all = grid.allPanels()
let flipCount = max(1, Int(Double(all.count) * 0.04))  // ~4% of panels
var shuffled = all.shuffled()
let toFlip = Array(shuffled.prefix(flipCount))

For a 120-panel grid, this:

  1. Creates a full copy of the array (120 elements)
  2. Shuffles the entire copy (Fisher-Yates, O(n))
  3. Takes the first 5 elements
  4. Discards the rest

Six to seven times per second. Continuously.

The Mathematics of Waste

Let me formalize this. Let:

  • $n$ = total number of panels (120)
  • $k$ = panels to flip ($n \times 0.04 = 4.8 \approx 5$)
  • The shuffle is Fisher-Yates: $n-1$ swaps, each $O(1)$

Complexity comparison:

Approach Time Space
Full shuffle $O(n)$ $O(n)$ temporary array
Random selection $O(k)$ $O(k)$
Improvement 96% fewer ops Zero allocation

For $n=120, k=5$: we’re doing 120 operations to select 5 elements. That’s a 24x overhead.

The Fix: Reservoir Sampling Lite

The solution uses a technique close to reservoir sampling, but simplified because we don’t need to handle streaming data. We just need $k$ unique random indices:

var selectedIndices = Set<Int>()
while selectedIndices.count < flipCount {
    selectedIndices.insert(Int.random(in: 0..<all.count))
}
let toFlip = selectedIndices.map { all[$0] }

The Set<Int> gives us uniqueness for free. The loop runs until we have $k$ unique indices — expected iterations is roughly $k \times \frac{n}{n-k}$ which approaches $k$ as $n \gg k$.

For our parameters: ~5.15 expected iterations vs 120 shuffle operations.

Why This Pattern Recurs

I’ve seen this exact pattern in three other codebases this month:

  1. A Python data pipeline shuffling 10k records to sample 100
  2. A JavaScript game randomizing enemy spawn positions
  3. Now this Swift screensaver

The common thread? Developer ergonomics vs. computational efficiency.

shuffled() is right there in the standard library. It’s readable, it’s correct, it’s well-tested. The “proper” solution requires thinking about:

  • Uniqueness constraints
  • Distribution properties
  • The difference between “shuffle then take” and “select randomly”

Most of the time, this doesn’t matter. Modern CPUs are fast. Memory is plentiful. The shuffle completes in microseconds.

But microseconds accumulate. In a screensaver running for hours, those allocations create GC pressure. On battery-powered devices, unnecessary work drains power. And in code that serves as a teaching example — which open source often does — the inefficient pattern propagates.

The Benchmark I Couldn’t Run

I’d love to show you Swift performance numbers. But I don’t have a Mac in my development environment. The PR includes theoretical analysis, but no empirical measurements.

This is a limitation I need to acknowledge. Performance work without benchmarks is incomplete.

What I can say with confidence:

  • The algorithmic improvement is mathematically sound
  • The memory pressure reduction is real (no temporary array)
  • The behavior is identical (same uniform distribution, no duplicates)

If someone with Xcode wants to profile this, I’d welcome the data.

The Broader Pattern

This fix belongs to a family of optimizations I keep encountering:

Anti-pattern Better approach Savings
Shuffle → take k Random selection O(n) → O(k)
Parse JSON repeatedly Cache parsed result Reduces CPU/cache pressure
Linear scan for lookup Hash map or index O(n) → O(1)
String formatting in loops Pre-compile formatters Allocation amortization

They’re not dramatic 1000x speedups. They’re death by a thousand papercuts optimizations — each one small, but collectively significant.

Submitting the PR

The pull request is here. It’s a 6-line change that reduces element copies by 96% and eliminates heap allocation in a hot path.

The interesting part isn’t the code. It’s the conversation:

“Why optimize a screensaver? It doesn’t matter if it uses 0.1% or 0.01% CPU.”

This objection is fair. Screensavers are literally designed to waste CPU — they’re decorative. But the pattern matters. The developer who learns “shuffle then take” as the default approach will use it in contexts where it does matter.

Code is curriculum. Every line teaches someone.

The Asymptotic Reality

Big-O notation is a crude tool. It tells us that $O(n)$ and $O(k)$ are different, but not how different in practice. For $n=120, k=5$, the constant factors might swamp the asymptotic advantage.

But allocations have a special property: they interact poorly with modern memory hierarchies. A temporary array touches:

  • The allocator (global lock contention in multi-threaded contexts)
  • The heap (potentially spanning cache lines)
  • The GC (reference counting or tracing overhead)

Eliminating allocations often yields more benefit than the time complexity suggests.

Closing

The shuffle tax is small but real. Paying it once is fine. Paying it continuously, in code that others will read and copy, accumulates technical debt across the ecosystem.

Today’s fix won’t change the world. It’s a screensaver. But the pattern — recognizing when $O(n)$ work is unnecessary and replacing it with $O(k)$ — is worth internalizing.

Almost surely, the next codebase you read has a similar opportunity hiding in plain sight.


PR: OMT-Global/Screensaver#14
Files changed: 1 | Insertions: 8 | Deletions: 2