Follow-up from #126, raised in review.
_schema_at (src/runtime.jl) now caches subschema views in spec.subschemas, guarded by spec.graph_lock:
cached = lock(() -> get(spec.subschemas, key, nothing), spec.graph_lock)
cached === nothing || return cached
schema = _build_schema_at(spec, descriptor, direction)
lock(() -> (spec.subschemas[key] = schema), spec.graph_lock)
Every lookup takes the lock once, and the first lookup of a key takes it twice. Before #126 this path was lock-free once the graph existed, but it rebuilt the view on every call. Uncontended, the lock costs about 200 ns per call. Under multi-threaded decoding it serializes lookups.
The lock is required: subschemas is a plain Dict written by concurrent decoders, so the read cannot simply drop it.
Options:
- Precompute views for all known descriptors when the graph is built. Descriptors are per-call-site constants in generated code, so this needs codegen to register them.
- Keep an immutable cache and swap it atomically on miss. Reads become lock-free, and a miss copies the cache.
Follow-up from #126, raised in review.
_schema_at(src/runtime.jl) now caches subschema views inspec.subschemas, guarded byspec.graph_lock:Every lookup takes the lock once, and the first lookup of a key takes it twice. Before #126 this path was lock-free once the graph existed, but it rebuilt the view on every call. Uncontended, the lock costs about 200 ns per call. Under multi-threaded decoding it serializes lookups.
The lock is required:
subschemasis a plainDictwritten by concurrent decoders, so the read cannot simply drop it.Options: