perf: cache UDT struct field mappings to avoid per-row tag scanning (+small bug fix in marshalUDT)#880
perf: cache UDT struct field mappings to avoid per-row tag scanning (+small bug fix in marshalUDT)#880mykaul wants to merge 2 commits into
Conversation
e0db6f9 to
b47facf
Compare
b47facf to
f654955
Compare
📝 WalkthroughWalkthroughThis PR optimizes UDT (User-Defined Type) marshaling and unmarshaling by introducing a sync.Map-based cache for struct field resolution. Previously, both 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review: UDT struct field-mapping cache (perf/udt-field-cache)Deep review of the UDT field-cache change. Summary: the cache design is correct; I found one latent bug fix the change happens to introduce, and one gap (the PR added only benchmarks, no correctness unit tests). I added correctness tests (test-only) and verified everything passes under Findings (by severity)LOW (resolved by the change — note for awareness): Pre-cache LOW (test gap): The PR added two benchmarks but no correctness unit tests for the cache. Addressed below. No issues found in:
Changes I made (TEST-ONLY — no hot-path edits)
Test results
RebaseRebased onto Push decisionPushed ( Benchmark requirement (author's hot-path)The cache itself is a hot-path change and per the agreed policy still needs benchmark validation before merge (machine was busy; not run here at scale). Recommended: Smoke run ( |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@marshal_test.go`:
- Around line 1551-1556: The benchmark input is wrapped with an extra
bytesWithLength call causing unmarshalUDT to treat the entire UDT payload as a
single field; remove the outer bytesWithLength and construct data as the
concatenation of the four already length-prefixed fields so unmarshalUDT will
iterate over all 4 fields (update the data assignment that currently calls
bytesWithLength(...) with nested bytesWithLength calls to instead concatenate
the individual bytesWithLength(...) results).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d265f3a0-1036-4afb-a750-b44a01b410a0
📒 Files selected for processing (2)
marshal.gomarshal_test.go
Both marshalUDT and unmarshalUDT previously rebuilt a map from struct tags on every call. For a struct with N fields, this allocated a map and iterated all fields+tags per UDT column per row. Replace with a sync.Map-based cache keyed by reflect.Type that maps CQL tag names to field indices. The mapping is built once per struct type and reused on all subsequent marshal/unmarshal calls. Also fixes a latent bug in marshalUDT where reflect.TypeOf(value) was used instead of k.Type(), which would panic if marshalUDT were ever called directly with a pointer-to-struct (currently unreachable via the Marshal wrapper which auto-dereferences pointers). Benchmark (4-field UDT struct, 4 elements): Unmarshal: ~324 ns/op -> ~121 ns/op (2.7x faster) Marshal: ~462 ns/op -> ~257 ns/op (1.8x faster)
Adds correctness unit tests for the UDT struct field-mapping cache: - distinct struct types keyed by reflect.Type do not collide - same Go type reused across different UDT definitions - untagged field FieldByName fallback preserved - extra UDT field (no matching struct field) skipped - pointer-to-struct marshal works (pre-cache code panicked here) - concurrent marshal/unmarshal of same type (race coverage) Test-only change; no hot-path logic modified.
f654955 to
69f02ed
Compare
Update — addressed CodeRabbit findingFixed the malformed benchmark input in
The fix was squashed into the perf commit that introduced the benchmark. |
Summary
marshalUDTandunmarshalUDTpreviously rebuilt amap[string]reflect.Valuefrom struct tags on every call — allocating a map and scanning all struct fields per UDT column per row.sync.Map-based cache (udtFieldCache) keyed byreflect.Type, mapping CQL tag names to field indices. Built once per struct type, reused forever.marshalUDTwherereflect.TypeOf(value)was used instead ofk.Type(), which would panic ifmarshalUDTwere called directly with a pointer-to-struct.Benchmark (4-field UDT struct)
Testing
All unit tests pass (
-race -tags unit, 10665 tests across 42 packages). Includes newBenchmarkUnmarshalUDTStructandBenchmarkMarshalUDTStruct.