🔥 perf(bind): eliminate double reflection in mergeStruct (-10% allocs) - #4385
Conversation
…cs/op mergeStruct() called isZero(dstField.Interface()) which: 1. Boxed each struct field to any (heap allocation per field) 2. Called reflect.ValueOf(value) (second reflection) Replaced with dstField.IsZero() — direct call on reflect.Value, zero interface boxing, zero extra reflection. benchmark old allocs/op new allocs/op delta Benchmark_BindAll_ReflectOverhead 119 107 -10.1% benchmark old B/op new B/op delta Benchmark_BindAll_ReflectOverhead 5030 4846 -3.7% name old allocs/op new allocs/op delta _BindAll_ReflectOverhead-12 119.0 ± 0% 107.0 ± 0% -10.08% (p=0.000 n=6+6) name old B/op new B/op delta _BindAll_ReflectOverhead-12 5.030Ki ± 0% 4.846Ki ± 0% -3.66% (p=0.000 n=6+6) Also removed the now-unused isZero helper function.
|
Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. If you need help or want to chat with us, join us on Discord https://gofiber.io/discord |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThe ChangesMerge Struct Zero-Check Optimization
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" 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 |
There was a problem hiding this comment.
Code Review
This pull request optimizes the mergeStruct function in bind.go by replacing the custom isZero helper function with a direct call to dstField.IsZero(), which avoids interface boxing and reflection overhead. The reviewer suggested further optimizing this block by checking dstField.CanSet() first to avoid calling IsZero() on unexported fields, and removing the redundant srcField.IsValid() check.
| if dstField.IsZero() { | ||
| if dstField.CanSet() && srcField.IsValid() { | ||
| dstField.Set(srcField) | ||
| } | ||
| } |
There was a problem hiding this comment.
We can optimize and simplify this block further:
- Check
dstField.CanSet()first: If a field cannot be set (e.g., it is unexported), we do not need to check if it is zero. CheckingCanSet()first avoids callingIsZero()on unexported fields. - Remove redundant
srcField.IsValid()check: Sincesrcanddstare of the exact same struct type (astempStructis instantiated withreflect.New(outElem.Type())),src.Field(i)on line 464 is guaranteed to return a validreflect.Value. Furthermore, ifsrcwere invalid,src.Field(i)would have already panicked on line 464 before reaching this check.
if dstField.CanSet() && dstField.IsZero() {
dstField.Set(srcField)
}
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4385 +/- ##
==========================================
- Coverage 91.38% 91.38% -0.01%
==========================================
Files 132 132
Lines 13113 13110 -3
==========================================
- Hits 11983 11980 -3
Misses 712 712
Partials 418 418
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Problem
mergeStruct()inbind.gousesisZero(dstField.Interface())to check if a struct field is zero-valued before copying from the source. This triggers two unnecessary reflection operations per field per source:dstField.Interface()— boxes thereflect.Valuetoany, causing a heap allocation for non-trivial typesreflect.ValueOf(value)— performs a second reflection lookup on the value we just boxedFor
Bind.All()which merges across 5 sources (URI, Body, Query, Header, Cookie) on a 10-field struct, this is ~50 boxing allocations per request that serve no purpose.Fix
Replace
isZero(dstField.Interface())withdstField.IsZero()— a direct method onreflect.Valuethat checks zero-ness without interface boxing or secondary reflection.Benchmark Results
Measured with
go test -bench=Benchmark_Confirm_BindAll_ReflectOverhead -benchmem -count=6on AMD Ryzen 5 7600X, Go 1.26.2.Checklist
make test— 3449 tests, 2 skipped)make lint— 0 issues)make format)betteralignpassesgeneratepassesisZerohelper function