Skip to content

🔥 perf(bind): eliminate double reflection in mergeStruct (-10% allocs) - #4385

Merged
ReneWerner87 merged 1 commit into
gofiber:mainfrom
pageton:perf/bind-iszero-reflect
May 29, 2026
Merged

ReneWerner87 merged 1 commit into
gofiber:mainfrom
pageton:perf/bind-iszero-reflect

Conversation

@pageton

@pageton pageton commented May 29, 2026

Copy link
Copy Markdown
Contributor

Problem

mergeStruct() in bind.go uses isZero(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:

  1. dstField.Interface() — boxes the reflect.Value to any, causing a heap allocation for non-trivial types
  2. reflect.ValueOf(value) — performs a second reflection lookup on the value we just boxed

For 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()) with dstField.IsZero() — a direct method on reflect.Value that checks zero-ness without interface boxing or secondary reflection.

// Before (2 reflection ops per field)
if isZero(dstField.Interface()) { ... }
func isZero(value any) bool {
    v := reflect.ValueOf(value)
    return v.IsZero()
}

// After (1 reflection op per field, no boxing)
if dstField.IsZero() { ... }

Benchmark Results

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)

Measured with go test -bench=Benchmark_Confirm_BindAll_ReflectOverhead -benchmem -count=6 on AMD Ryzen 5 7600X, Go 1.26.2.

Checklist

  • Tests pass (make test — 3449 tests, 2 skipped)
  • Lint passes (make lint — 0 issues)
  • Format passes (make format)
  • betteralign passes
  • generate passes
  • No new allocations introduced
  • Removed unused isZero helper function

…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.
@pageton
pageton requested a review from a team as a code owner May 29, 2026 17:21
@welcome

welcome Bot commented May 29, 2026

Copy link
Copy Markdown

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

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c27bd7a3-f708-48bb-8227-94fdfa4a6706

📥 Commits

Reviewing files that changed from the base of the PR and between 6bdef4b and dcba436.

📒 Files selected for processing (1)
  • bind.go

Walkthrough

The mergeStruct function in bind.go is optimized to check if a destination field is already populated by calling reflect.Value.IsZero() directly, instead of boxing the field value through Interface() and then calling the isZero helper. This change reduces overhead in the Bind.All() merge path and prevents potential panics when processing unexported fields.

Changes

Merge Struct Zero-Check Optimization

Layer / File(s) Summary
Direct IsZero() in mergeStruct
bind.go
The destination field "already set" check in mergeStruct is refactored from isZero(dstField.Interface()) to dstField.IsZero(), eliminating boxing overhead and avoiding panics on unexported fields in the Bind.All() merge path.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

  • gofiber/fiber#3373: Introduces the original mergeStruct function whose zero-detection mechanism is now optimized in this PR.

Suggested labels

⚡️ Performance

Suggested reviewers

  • gaby
  • sixcolors
  • ReneWerner87
  • efectn

Poem

🐰 A struct field check, once slow and boxing,
Now bounds direct through IsZero()—no locking!
Unexported fields panic no more,
Performance soars through Bind.All() door.
Tiny change, mighty gain at core! ⚡

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main optimization: replacing double reflection in mergeStruct with a direct IsZero() call, achieving ~10% allocation reduction.
Description check ✅ Passed The description provides a clear problem statement, detailed explanation of the fix with code examples, benchmark results, and a completed checklist. All critical sections are present and well-documented.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ReneWerner87 ReneWerner87 added this to v3 May 29, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone May 29, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread bind.go
Comment on lines +469 to 473
if dstField.IsZero() {
if dstField.CanSet() && srcField.IsValid() {
dstField.Set(srcField)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

We can optimize and simplify this block further:

  1. 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. Checking CanSet() first avoids calling IsZero() on unexported fields.
  2. Remove redundant srcField.IsValid() check: Since src and dst are of the exact same struct type (as tempStruct is instantiated with reflect.New(outElem.Type())), src.Field(i) on line 464 is guaranteed to return a valid reflect.Value. Furthermore, if src were invalid, src.Field(i) would have already panicked on line 464 before reaching this check.
		if dstField.CanSet() && dstField.IsZero() {
			dstField.Set(srcField)
		}

@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.38%. Comparing base (6bdef4b) to head (dcba436).

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              
Flag Coverage Δ
unittests 91.38% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ReneWerner87
ReneWerner87 merged commit ee98695 into gofiber:main May 29, 2026
20 checks passed
@github-project-automation github-project-automation Bot moved this to Done in v3 May 29, 2026
@ReneWerner87 ReneWerner87 modified the milestones: v3, v3.4.0 Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants