Sentinel & Special Values — Optimization Drills¶
Category: Resource & Type-Safety Patterns — when a sentinel is the performance answer and when a wrapper costs nothing.
8 drills weighing correctness against allocation and branch cost.
Apple M2 Pro, single thread. Numbers indicative, not authoritative.
Table of Contents¶
- Drill 1: Keep
-1on a Hot Search Path - Drill 2:
OptionalIntover BoxedOptional<Integer> - Drill 3: Let Escape Analysis Erase
Optional - Drill 4:
(value, ok)Instead of an Allocating Wrapper (Go) - Drill 5: Don't Wrap Sentinel Errors in Hot Loops
- Drill 6: Sentinel Node to Delete Branches
- Drill 7: Length-Prefix over Null-Terminator Scans
- Drill 8: Branchless NaN Filtering
Drill 1: Keep -1 on a Hot Search Path¶
"Safe" but costly¶
Optional<Integer> find(int[] a, int key) {
for (int i = 0; i < a.length; i++)
if (a[i] == key) return Optional.of(i); // boxes when stored/escaped
return Optional.empty();
}
In a tight inner loop where the result escapes (stored, collected), each hit allocates an Integer + Optional.
Optimized — out-of-domain sentinel¶
int find(int[] a, int key) {
for (int i = 0; i < a.length; i++)
if (a[i] == key) return i;
return -1; // provably out of the index domain → no ambiguity, no alloc
}
Lesson: A sentinel is the correct optimization precisely when it is out-of-domain and the wrapper allocation is measurable. -1 for an index qualifies.
Drill 2: OptionalInt over Boxed Optional<Integer>¶
Slow¶
Optimized¶
OptionalInt firstLow(int[] xs, int t) {
for (int i = 0; i < xs.length; i++)
if (xs[i] < t) return OptionalInt.of(i);
return OptionalInt.empty();
}
Benchmark¶
Lesson: When you want explicit absence and primitives, the primitive Optionals remove the boxing that pushes teams back toward -1.
Drill 3: Let Escape Analysis Erase Optional¶
Belief: "Optional always allocates"¶
Reality¶
The Optional is created and consumed in the same scope, so HotSpot scalar-replaces it — zero heap allocation post-JIT.
Benchmark¶
OptionalMapOrElse (escaped scope) avgt 1.3 ns/op 0 B/op
OptionalStoredInField avgt 8.0 ns/op 16 B/op
Lesson: Don't reach for a sentinel to avoid an Optional allocation that the JIT already elides. Only Optional stored in fields/collections truly allocates — and that usage is an anti-pattern anyway.
Drill 4: (value, ok) Instead of an Allocating Wrapper (Go)¶
Slow — pointer-as-optional¶
func lookup(m map[string]int, k string) *int {
if v, ok := m[k]; ok { return &v } // &v escapes → heap alloc
return nil
}
Optimized — comma-ok¶
func lookup(m map[string]int, k string) (int, bool) {
v, ok := m[k]
return v, ok // two registers, no allocation
}
Benchmark¶
Lesson: In Go, multiple return values are the zero-allocation out-of-band channel; *T-as-optional escapes to the heap.
Drill 5: Don't Wrap Sentinel Errors in Hot Loops¶
Slow¶
for _, k := range keys {
if _, err := get(k); errors.Is(err, ErrMiss) {
miss++ // get() does fmt.Errorf("...: %w", ErrMiss) → 48 B per miss
}
}
Optimized — return the bare sentinel on the hot path¶
func get(k string) (int, error) {
if v, ok := store[k]; ok { return v, nil }
return 0, ErrMiss // no wrapping → no allocation
}
Benchmark¶
Lesson: Wrap with %w for context at boundaries; return the bare sentinel error in hot loops. errors.Is works for both.
Drill 6: Sentinel Node to Delete Branches¶
Slow — branch per operation¶
void insertFront(int v) {
Node n = new Node(v);
if (head == null) { head = n; } // first-node special case
else { n.next = head; head = n; }
}
Optimized — dummy head sentinel¶
private final Node head = new Node(0); // sentinel, never null
void insertFront(int v) {
Node n = new Node(v);
n.next = head.next;
head.next = n; // unconditional — no branch
}
Lesson: A sentinel node removes the boundary branch from every insert/delete. The win is fewer mispredicted branches in the structure's hottest methods, paid for with one allocation at construction.
Drill 7: Length-Prefix over Null-Terminator Scans¶
Slow — \0 sentinel forces O(n) length¶
Repeated strlen in a loop is quadratic over a growing string.
Optimized — carry the length out of band¶
Lesson: A null-terminator is an in-band length sentinel with O(n) length and over-read risk. A length-prefixed representation moves the length to a separate channel — O(1) and safe. This is "stop overloading, widen the channel" at the memory layer.
Drill 8: Branchless NaN Filtering¶
Slow — NaN poisons, or a branch per element¶
Optimized — vectorized NaN-aware aggregation¶
import numpy as np
def mean(xs):
arr = np.asarray(xs, dtype=float)
return float(np.nanmean(arr)) # NaN excluded, SIMD, no Python-level branch
// Java: stream filter compiles to a tight, predictable loop.
double m = Arrays.stream(xs).filter(x -> !Double.isNaN(x)).average().orElse(Double.NaN);
Lesson: NaN is a sentinel inside the numeric domain; it must be filtered explicitly. Vectorized nan* functions do it without per-element interpreter branches.
Optimization Tips¶
How to decide sentinel vs wrapper¶
- Is the sentinel out-of-domain? If not, you have no choice — go out-of-band for correctness, performance be damned.
- Does the wrapper actually allocate here? Check escape analysis (Java JFR/
-XX:+PrintInlining, Go-gcflags=-m). Often it doesn't. - Is this path actually hot? Profile (
async-profiler,pprof,py-spy) before trading clarity for a sentinel.
Checklist¶
- Keep out-of-domain sentinels (
-1index,EOF) on proven hot paths. - Use
OptionalInt/(value, ok)to avoid boxing without overloading a value. - Trust escape analysis to erase non-escaping
Optionals. - Don't wrap sentinel errors in hot loops; wrap at boundaries.
- Use sentinel nodes to delete boundary branches.
- Prefer length-prefixed over null-terminated for repeated length queries.
- Filter
NaNexplicitly, ideally vectorized.
Anti-optimizations¶
- ❌ Overloading
0/""/-1-on-a-signed-int to "save an allocation" — buys a silent-corruption bug. - ❌ Reaching for a sentinel to dodge an
Optionalthe JIT already elides. - ❌ Pointer-as-optional in Go (
*T) — it escapes; use(value, ok). - ❌ Exposing a sentinel node across an API to avoid a check.
Summary¶
Sentinels are the right optimization only when they are out of the valid domain and the wrapper cost is real and on a hot path. Most of the time the wrapper (Optional, (value, ok)) is free or near-free — escape analysis, primitive Optionals, and multiple returns remove the cost without the ambiguity. The genuine performance wins live in sentinel nodes (branch deletion) and length-prefixed representations (O(1) length), not in overloading valid values.
← Find-Bug · Resource & Type-Safety · Roadmap
Sentinel & Special Values complete. All 8 files: junior · middle · senior · professional · interview · tasks · find-bug · optimize.
In this topic