Experiment 176: Row.containsKey identity fast path

Date: 2026-06-16

Status: In Review

Direction:result-transfer-shape

Benchmark Run: Focused row_map_facade A/B (order-stable, 3 passes per side, quiet box) + release-suite Select → Maps artifact

Problem

Exp 158 added a schema-name identity fast path to RowSchema.indexOf: for

schemas with at most 32 columns, a lookup first does a short

identical(names[i], key) scan and only falls back to the private

HashMap<String, int> index when the supplied key is equal-but-non-identical

(or the schema is wider than 32 columns). That cut row_map_facadehot lookup

−52% because the common full-consumption pattern (for (final key in row.keys) row[key])

re-feeds the schema's own interned name objects, so the identity scan hits before

any hashing.

Row.containsKey, however, was never routed through that fast path. It went

straight to the raw map:

 bool containsKey(Object? key) => key is String && _schema._indexByName.containsKey(key); 

So every containsKey call hashed the key string, even when the caller passed

an interned column name that an identical() scan would resolve without

hashing. Exp 158's results table lists containsKey as "neutral" — but that

row compared Row vs a LinkedHashMap with containsKeyunchanged on both

sides of exp 158's diff. It is not evidence that routing containsKey

through the identity path is neutral; that path was simply never applied to

containsKey.

A fresh baseline confirms the gap. On the row_map_facade benchmark (8-column

schema, key 'updated_at' supplied as a string literal that is identical to

names[5]):

laneRow median (baseline)LinkedHashMap median
hot lookup (operator[], exp 158 path)~3.95 ms~5.25 ms
containsKey (raw HashMap)~13.0 ms~9.35 ms

containsKey does strictly less work than hot lookup (one membership probe

vs two value lookups) yet ran ~3.3× slower and was ~+3.6 ms slower than a

plain LinkedHashMap — the direct signature of paying the HashMap hash that

the identity scan was built to skip.

Hypothesis

Routing Row.containsKey through the exp 158 identity fast path (via a shared

RowSchema.containsName(name) => indexOf(name) >= 0 helper) will reproduce the

operator[] win on the containsKey lane for the interned-key case: drop the

Row containsKey median toward the hot lookup level and flip it from slower

to at-parity-or-faster than LinkedHashMap, with no public API change and no

movement on the hot lookup control lane.

Behavior is identical: indexOf returns the column index when the name is

present (by identity or by map fallback) and -1 otherwise, so >= 0 is

exactly membership. The negative case (containsKey('nonexistent')) still falls

through the identity miss to the HashMap and returns -1 >= 0false.

Approach

lib/src/row.dart:

existing identity-then-HashMap path.

_schema._indexByName.containsKey(key). Row no longer reaches into the

private map field directly — both operator[] and containsKey go through

the schema's lookup methods.

One-line behavioral change plus a documented helper; no new fields, no

allocation change, no public surface change.

Results

Focused row_map_facade A/B, fresh worktree off origin/main, three passes per

side on a quiet box. The hot lookup lane is the control (unchanged by this

diff).

lanebaseline Row (3 passes)candidate Row (3 passes)delta
containsKey12.869 / 13.006 / 13.192 ms10.027 / 10.039 / 9.941 ms−23%
hot lookup (control)3.889 / 3.962 / 3.997 ms4.038 / 3.936 / 3.922 msneutral

Row-vs-LinkedHashMap on the containsKey lane flipped from +3.6 ms slower

(baseline) to roughly at-parity (candidate delta −0.9 / +0.8 / +0.8 ms across

passes — straddling zero). The control lane is flat across the diff, so the

movement is the targeted path, not a machine-wide shift.

Honest framing of the size of the win:

containsKey (~10 ms) is still well above hot lookup (~3.9 ms) because

containsKey still pays the MapMixin.containsKey dispatch and the

key is String type check that operator[] does not. So this is a clean

−23%, not the −52% exp 158 measured on operator[].

fast path. In the real decode path the schema names come from

fastDecodeText (freshly allocated, non-canonical strings), so a

user-supplied string literal will generally not be identical to a

decoded name and containsName will fall through to the HashMap — the same

cost as before, never worse. The benchmark hits the identity path because both

the schema and the probe key are string literals (canonicalized to the same

object). The win materializes for callers that re-feed row.keys or for the

common literal-vs-literal schema (e.g. RowSchema built from constant names),

not for arbitrary literal-vs-decoded-name probes.

Release-suite Select → Maps artifact

(benchmark/results/<date>-exp176-containskey-identity.md) is committed as the

public dated lane. It does not isolate containsKey (the suite iterates fields,

it does not membership-probe), so it is expected to be neutral there — the

focused facade A/B carries the signal.

Decision

Accept for review.

Bounded one-line behavioral change that closes a real gap exp 158 left open:

Row.containsKey paid a HashMap hash on a path where its sibling operator[]

already had an identity shortcut. The change is behavior-identical, removes the

last direct _indexByName reach-out from Row, and turns the containsKey

lane from slower-than-LinkedHashMap to at-parity, reproducibly across three

order-stable passes with a flat control lane. Touches lib/, so it gets a human

glance per the disposition policy.

Reject calculus for a future runner: this is the right move while the identity

fast path exists at all. Reopen/revisit only if exp 158's identity scan itself

is removed or its 32-column cap changes — containsName simply inherits whatever

indexOf does, so the two stay coupled by construction.

Future Notes

identical to decoded schema names at high frequency (the identity miss case),

the cost reverts to the HashMap probe — same as before this change, never

worse. No additional work is warranted unless such a workload is shown to be

hot.

apply). It is a consistency fix inside the same private schema index.

Validation

containsKey('id') / containsKey('name') / containsKey('nonexistent')

assertions that exercise both the identity-hit and HashMap-fallback paths

(3 passes per side)