Skip to content

⚡ perf: reduce allocations in Request (saves ~16% B/op)#3768

Merged
ReneWerner87 merged 3 commits intogofiber:mainfrom
arturmelanchyk:headers
Sep 30, 2025
Merged

⚡ perf: reduce allocations in Request (saves ~16% B/op)#3768
ReneWerner87 merged 3 commits intogofiber:mainfrom
arturmelanchyk:headers

Conversation

@arturmelanchyk
Copy link
Contributor

@arturmelanchyk arturmelanchyk commented Sep 28, 2025

Description

Avoids unneeded allocations in Headers(), results according to Benchmark_Request_Headers

benchstat old.txt new.txt                                                          
goos: darwin
goarch: arm64
pkg: github.com/gofiber/fiber/v3/client
cpu: Apple M3 Max
                    │   old.txt   │            new.txt            │
                    │   sec/op    │   sec/op     vs base          │
_Request_Headers-16   186.9n ± 1%   185.2n ± 1%  ~ (p=0.311 n=20)

                    │  old.txt   │              new.txt               │
                    │    B/op    │    B/op     vs base                │
_Request_Headers-16   200.0 ± 0%   168.0 ± 0%  -16.00% (p=0.000 n=20)

                    │  old.txt   │            new.txt             │
                    │ allocs/op  │ allocs/op   vs base            │
_Request_Headers-16   7.000 ± 0%   7.000 ± 0%  ~ (p=1.000 n=20) ¹
¹ all samples are equal

Changes introduced

List the new features or adjustments introduced in this pull request. Provide details on benchmarks, documentation updates, changelog entries, and if applicable, the migration guide.

  • Benchmarks: Describe any performance benchmarks and improvements related to the changes.
  • Documentation Update: Detail the updates made to the documentation and links to the changed files.
  • Changelog/What's New: Include a summary of the additions for the upcoming release notes.
  • Migration Guide: If necessary, provide a guide or steps for users to migrate their existing code to accommodate these changes.
  • API Alignment with Express: Explain how the changes align with the Express API.
  • API Longevity: Discuss the steps taken to ensure that the new or updated APIs are consistent and not prone to breaking changes.
  • Examples: Provide examples demonstrating the new features or changes in action.

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)
  • Enhancement (improvement to existing features and functionality)
  • Documentation update (changes to documentation)
  • Performance improvement (non-breaking change which improves efficiency)
  • Code consistency (non-breaking change which improves code reliability and robustness)

Checklist

Before you submit your pull request, please make sure you meet these requirements:

  • Followed the inspiration of the Express.js framework for new functionalities, making them similar in usage.
  • Conducted a self-review of the code and provided comments for complex or critical parts.
  • Updated the documentation in the /docs/ directory for Fiber's documentation.
  • Added or updated unit tests to validate the effectiveness of the changes or new features.
  • Ensured that new and existing unit tests pass locally with the changes.
  • Verified that any new dependencies are essential and have been agreed upon by the maintainers/community.
  • Aimed for optimal performance with minimal allocations in the new code.
  • Provided benchmarks for the new code to analyze and improve upon.

Commit formatting

Please use emojis in commit messages for an easy way to identify the purpose or intention of a commit. Check out the emoji cheatsheet here: CONTRIBUTING.md

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 28, 2025

Walkthrough

The request header iteration in client/request.go now uses immutable Go strings for header keys instead of byte slices; PeekAll and yield calls receive string keys directly. Values remain as byte slices and per-value conversions are unchanged; overall control flow is preserved.

Changes

Cohort / File(s) Summary
Header key handling refactor
client/request.go
Replaced intermediate header key representation from []byte to string; updated calls to PeekAll and yield to pass string keys directly; removed byte-slice conversions while preserving existing iteration and value handling.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant R as Request.Headers()
    participant H as HeaderStore
    participant C as Caller (yield)

    Note over R,H: iterate header keys as strings
    R->>H: Keys() -> []string
    loop for each key (string)
        R->>H: PeekAll(key:string)
        H-->>R: values []byte[]
        R->>C: yield(key:string, values)
        alt yield returns false
            R-->>R: break
        end
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested labels

🧹 Updates

Suggested reviewers

  • gaby
  • sixcolors
  • efectn

Poem

I twitch my ears at strings so neat,
No byte-slice crumbs beneath my feet.
I peek and yield with nimble hop,
Keys now strings — I skip, then stop. 🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "⚡ perf: reduce allocations in Request (saves ~16% B/op)" clearly and accurately summarizes the main change in the pull request. The changeset modifies the Headers() method in client/request.go to reduce memory allocations by switching from byte-slice handling to direct string operations, which directly corresponds to the performance improvement stated in the title. The title is concise, specific, and conveys the primary benefit (16% reduction in bytes per operation) that a developer would want to know when reviewing the commit history. While it includes an emoji, this appears to be part of the repository's established commit formatting conventions as referenced in their CONTRIBUTING.md guide.
Description Check ✅ Passed The pull request description follows the repository template structure and includes most required sections. The author has provided a clear description of the changes with benchmark results showing the 16% B/op reduction, correctly identified the type of change as a performance improvement, and marked the relevant checklist item for benchmarks. However, several checklist items in the "Checklist" section remain unchecked (self-review, unit tests, ensuring tests pass), and some sections like "Changes introduced" have multiple unchecked items that may be relevant such as changelog entries. While the description is substantially complete and provides the core information needed to understand the change, some non-critical sections could be more thoroughly addressed.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

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

@codecov
Copy link

codecov bot commented Sep 28, 2025

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.54%. Comparing base (e58fec7) to head (bb834ad).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
client/request.go 87.50% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3768   +/-   ##
=======================================
  Coverage   91.54%   91.54%           
=======================================
  Files         113      113           
  Lines       11912    11915    +3     
=======================================
+ Hits        10905    10908    +3     
  Misses        740      740           
  Partials      267      267           
Flag Coverage Δ
unittests 91.54% <87.50%> (+<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.

@arturmelanchyk arturmelanchyk marked this pull request as ready for review September 28, 2025 16:53
@arturmelanchyk arturmelanchyk requested a review from a team as a code owner September 28, 2025 16:53
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
client/request.go (1)

156-158: Doc lifetime mismatch with unsafe conversions

Comment claims validity “until the request object is released,” but with unsafe aliasing it may be only until the next header call. If you keep UnsafeString anywhere, tighten the docs; if you adopt copying as suggested, the current wording is OK.

Possible tweak if keeping unsafe conversions:

-// The returned values are only valid until the request object is released.
-// Do not store references to returned values; make copies instead.
+// Returned keys/values may alias fasthttp internal buffers and are valid only during this iteration.
+// Do not store references; copy if you need to retain them beyond the callback.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd273d2 and 13ed4dd.

📒 Files selected for processing (1)
  • client/request.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Format Go code using gofumpt (enforced via make format)
Ensure code passes golangci-lint checks (enforced via make lint)
Optimize struct field alignment using betteralign (enforced via make betteralign)
Modernize Go code using gopls modernize (enforced via make modernize)

Files:

  • client/request.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Compare

Comment on lines +162 to +166
// It is necessary to have immutable byte slice.
keys := make([]string, len(peekKeys))
for i, key := range peekKeys {
keys[i] = utils.UnsafeString(key)
}
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Unsafe key aliasing: UnsafeString defeats the “immutable” guarantee here

utils.UnsafeString(key) returns a string aliasing fasthttp’s internal byte buffer. If RequestHeader reuses/mutates buffers across subsequent header ops, these strings can change underneath you during iteration. Copy to string to ensure stability.

Apply this diff:

-		// It is necessary to have immutable byte slice.
-		keys := make([]string, len(peekKeys))
-		for i, key := range peekKeys {
-			keys[i] = utils.UnsafeString(key)
-		}
+		// Copy keys to immutable strings to decouple from fasthttp's internal buffers.
+		keys := make([]string, len(peekKeys))
+		for i, key := range peekKeys {
+			keys[i] = string(key) // copy; avoids aliasing when fasthttp reuses buffers
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// It is necessary to have immutable byte slice.
keys := make([]string, len(peekKeys))
for i, key := range peekKeys {
keys[i] = utils.UnsafeString(key)
}
// Copy keys to immutable strings to decouple from fasthttp's internal buffers.
keys := make([]string, len(peekKeys))
for i, key := range peekKeys {
keys[i] = string(key) // copy; avoids aliasing when fasthttp reuses buffers
}
🤖 Prompt for AI Agents
client/request.go lines 162-166: the code uses utils.UnsafeString(key) which
aliases fasthttp's internal byte buffer and can change under you; replace
UnsafeString with an explicit copy of the key bytes before converting to string
so each entry is immutable (e.g. allocate a new byte slice, copy the contents of
key into it, then convert that slice to string) and assign that copied string
into keys[i].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I follow the original approach with utils.UnsafeString(key) because suggested string(key)performs allocation

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@arturmelanchyk You're absolutely right that string(key) performs an allocation, while utils.UnsafeString(key) avoids it by creating a string header that points directly to the underlying byte slice.


@arturmelanchyk You make a valid point about allocation trade-offs. Looking at this more carefully, I can see the reasoning behind your approach.

Your performance argument is solid: string(key) does indeed perform an allocation and copy, while utils.UnsafeString(key) creates a string header pointing directly to the existing byte slice without copying.

The context matters here: In your Headers() iterator, the keys are:

  1. Converted immediately during the function setup
  2. Used within the same call scope
  3. Not stored beyond the method's lifetime

The benchmark results support your decision: You're achieving a 16% reduction in bytes per operation (200 B/op → 168 B/op), which is significant for a performance-critical HTTP library.

Risk assessment: While there is a theoretical risk if fasthttp reuses the underlying buffers during iteration, the usage pattern here (immediate iteration, no long-term storage) makes this less likely to cause issues in practice.

Given that this is a high-performance HTTP library where allocation reduction is a primary goal, and your benchmarks show measurable improvement, your choice to use utils.UnsafeString(key) is reasonable. The performance benefit outweighs the theoretical safety concern in this specific context.

The comment could be updated to be more explicit about the trade-off:

// Convert to strings immediately while buffers are stable
keys := make([]string, len(peekKeys))
for i, key := range peekKeys {
    keys[i] = utils.UnsafeString(key) // zero-copy conversion, valid for iteration scope
}

✏️ Learnings added
Learnt from: arturmelanchyk
PR: gofiber/fiber#3768
File: client/request.go:162-166
Timestamp: 2025-09-28T17:39:26.607Z
Learning: In fasthttp-based code like gofiber, using utils.UnsafeString(key) instead of string(key) is acceptable when values are used immediately within the same call stack and not stored for later use, as it avoids allocations and aligns with fasthttp's zero-allocation design philosophy.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +169 to +175
vals := r.header.PeekAll(key)
valsStr := make([]string, len(vals))
for i, v := range vals {
valsStr[i] = utils.UnsafeString(v)
}

if !yield(utils.UnsafeString(key), valsStr) {
if !yield(key, valsStr) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Values may also alias and mutate across iterations

If PeekAll reuses internal buffers between calls, valsStr[i] = utils.UnsafeString(v) can yield strings that change after the next PeekAll. Either copy each value or document the shorter lifetime explicitly.

Safer copy:

-			for i, v := range vals {
-				valsStr[i] = utils.UnsafeString(v)
-			}
+			for i, v := range vals {
+				valsStr[i] = string(v) // copy for stability across subsequent header calls
+			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vals := r.header.PeekAll(key)
valsStr := make([]string, len(vals))
for i, v := range vals {
valsStr[i] = utils.UnsafeString(v)
}
if !yield(utils.UnsafeString(key), valsStr) {
if !yield(key, valsStr) {
vals := r.header.PeekAll(key)
valsStr := make([]string, len(vals))
for i, v := range vals {
valsStr[i] = string(v) // copy for stability across subsequent header calls
}
if !yield(key, valsStr) {
🤖 Prompt for AI Agents
In client/request.go around lines 169-175, PeekAll may reuse internal buffers so
assigning valsStr[i] = utils.UnsafeString(v) can produce strings that later
mutate; fix by making a copy of each byte slice before converting to a string
(e.g., allocate a new byte slice and copy the contents, then convert that copied
slice to a safe string) or use a provided safe-copy helper; ensure vals are
fully copied into independent memory prior to calling yield.

@gaby gaby changed the title perf: reduce allocations in Request (saves ~16% B/op) ⚡ perf: reduce allocations in Request (saves ~16% B/op) Sep 28, 2025
@gaby gaby added this to v3 Sep 28, 2025
@gaby gaby added this to the v3 milestone Sep 28, 2025
@gaby gaby moved this to In Progress in v3 Sep 28, 2025
@gaby
Copy link
Member

gaby commented Sep 28, 2025

/gemini review

@gaby gaby requested a review from Copilot September 28, 2025 17:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR optimizes memory allocations in the Request.Headers() method to improve performance. The change reduces memory allocations by ~16% (from 200 B/op to 168 B/op) by eliminating redundant string conversions.

Key changes:

  • Convert byte slices to strings once during key processing instead of multiple times during iteration
  • Remove redundant utils.UnsafeString() calls in the yield operation

Copy link
Contributor

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

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 introduces a performance optimization in the Request.Headers() method by reducing memory allocations. The change replaces the creation of a temporary [][]byte slice for header keys with a more efficient []string slice. This is a good optimization as a string slice header is smaller than a byte slice slice header, reducing memory usage. The benchmark results confirm this improvement with a ~16% reduction in bytes per operation. The change is logical, well-contained, and improves performance. I have no further suggestions.

keys := make([][]byte, len(peekKeys))
copy(keys, peekKeys) // It is necessary to have immutable byte slice.

// It is necessary to have immutable byte slice.
Copy link
Member

@ReneWerner87 ReneWerner87 Sep 29, 2025

Choose a reason for hiding this comment

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

@arturmelanchyk the comment is a little bit confusing, since you create a byte slice with mutable values out of the key

Suggested change
// It is necessary to have immutable byte slice.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
client/request.go (1)

162-166: Update comment to accurately reflect zero-copy conversion.

The comment states "Copy keys to immutable strings to decouple from fasthttp's internal buffers," but utils.UnsafeString(key) performs a zero-copy conversion by aliasing the underlying byte slice—it does not actually copy. While this approach is valid for the performance goals (as confirmed by the 16% B/op reduction), the comment is misleading.

Consider updating the comment to accurately describe the behavior:

-		// Copy keys to immutable strings to decouple from fasthttp's internal buffers.
+		// Convert keys to strings for immediate use during iteration (zero-copy conversion).
 		keys := make([]string, len(peekKeys))
 		for i, key := range peekKeys {
 			keys[i] = utils.UnsafeString(key)
 		}

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13ed4dd and bb834ad.

📒 Files selected for processing (1)
  • client/request.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Format Go code using gofumpt (enforced via make format)
Ensure code passes golangci-lint checks (enforced via make lint)
Optimize struct field alignment using betteralign (enforced via make betteralign)
Modernize Go code using gopls modernize (enforced via make modernize)

Files:

  • client/request.go
🧠 Learnings (1)
📚 Learning: 2025-09-28T17:39:26.644Z
Learnt from: arturmelanchyk
PR: gofiber/fiber#3768
File: client/request.go:162-166
Timestamp: 2025-09-28T17:39:26.644Z
Learning: In fasthttp-based code like gofiber, using utils.UnsafeString(key) instead of string(key) is acceptable when values are used immediately within the same call stack and not stored for later use, as it avoids allocations and aligns with fasthttp's zero-allocation design philosophy.

Applied to files:

  • client/request.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Compare
  • GitHub Check: unit (1.25.x, windows-latest)
  • GitHub Check: repeated

@ReneWerner87 ReneWerner87 merged commit cb00374 into gofiber:main Sep 30, 2025
14 of 15 checks passed
@github-project-automation github-project-automation bot moved this from In Progress to Done in v3 Sep 30, 2025
Abhirup-99 pushed a commit to Abhirup-99/fiber that referenced this pull request Nov 3, 2025
Co-authored-by: Artur Melanchyk <13834276+arturmelanchyk@users.noreply.github.com>
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.

4 participants