Skip to content

perf: optimize path parsing using strings.Count #4246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

1911860538
Copy link
Contributor

@1911860538 1911860538 commented May 21, 2025

Refactored countParams and countSections to use strings.Count instead of converting strings to byte slices and using bytes.Count. This simplifies the code and results in a slight performance improvement.

Below is my local test code.

tree.go

package gin

import (
	"bytes"
	"net/url"
	"strings"
	"unicode"
	"unicode/utf8"

	"github.com/gin-gonic/gin/internal/bytesconv"
)

var (
	strColon = []byte(":")
	strStar  = []byte("*")
	strSlash = []byte("/")
)

func countParams(path string) uint16 {
	var n uint16
	s := bytesconv.StringToBytes(path)
	n += uint16(bytes.Count(s, strColon))
	n += uint16(bytes.Count(s, strStar))
	return n
}

func countParamsOptimized(path string) uint16 {
	colons := strings.Count(path, ":")
	stars := strings.Count(path, "*")
	return uint16(colons + stars)
}

func countSections(path string) uint16 {
	s := bytesconv.StringToBytes(path)
	return uint16(bytes.Count(s, strSlash))
}

func countSectionsOptimized(path string) uint16 {
	return uint16(strings.Count(path, "/"))
}

Sorry, this is the test code that was omitted in the previous submission.
tree_test.go

package gin

import (
	"fmt"
	"reflect"
	"regexp"
	"strings"
	"testing"
)

func Benchmark_countParams(b *testing.B) {
	path := "a/:name?page=1&pageSize=10"

	b.Run("original", func(b *testing.B) {
		b.ReportAllocs()
		for i := 0; i < b.N; i++ {
			countParams(path)
		}
	})

	b.Run("optimized", func(b *testing.B) {
		b.ReportAllocs()
		for i := 0; i < b.N; i++ {
			countParamsOptimized(path)
		}
	})
}

func Benchmark_countSections(b *testing.B) {
	path := "a/b/c/d"

	b.Run("original", func(b *testing.B) {
		b.ReportAllocs()
		for i := 0; i < b.N; i++ {
			countSections(path)
		}
	})

	b.Run("optimized", func(b *testing.B) {
		b.ReportAllocs()
		for i := 0; i < b.N; i++ {
			countSectionsOptimized(path)
		}
	})
}

benchmark output

goos: darwin
goarch: amd64
pkg: github.com/gin-gonic/gin
cpu: Intel(R) Core(TM) i7-8569U CPU @ 2.80GHz
Benchmark_countParams
Benchmark_countParams/original
Benchmark_countParams/original-8         	88666104	        12.36 ns/op	       0 B/op	       0 allocs/op
Benchmark_countParams/optimized
Benchmark_countParams/optimized-8        	94042611	        11.48 ns/op	       0 B/op	       0 allocs/op
PASS

Process finished with the exit code 0
goos: darwin
goarch: amd64
pkg: github.com/gin-gonic/gin
cpu: Intel(R) Core(TM) i7-8569U CPU @ 2.80GHz
Benchmark_countSections
Benchmark_countSections/original
Benchmark_countSections/original-8         	224172283	         5.353 ns/op	       0 B/op	       0 allocs/op
Benchmark_countSections/optimized
Benchmark_countSections/optimized-8        	243922398	         4.663 ns/op	       0 B/op	       0 allocs/op
PASS

Process finished with the exit code 0

@appleboy appleboy requested a review from Copilot May 22, 2025 00:26
Copy link

codecov bot commented May 22, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.92%. Comparing base (3dc1cd6) to head (50dcef5).
Report is 127 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4246      +/-   ##
==========================================
- Coverage   99.21%   98.92%   -0.30%     
==========================================
  Files          42       44       +2     
  Lines        3182     3429     +247     
==========================================
+ Hits         3157     3392     +235     
- Misses         17       26       +9     
- Partials        8       11       +3     
Flag Coverage Δ
?
--ldflags="-checklinkname=0" -tags sonic 98.85% <100.00%> (?)
-tags go_json 98.85% <100.00%> (?)
-tags nomsgpack 98.90% <100.00%> (?)
go-1.18 ?
go-1.19 ?
go-1.20 ?
go-1.21 ?
go-1.23 98.92% <100.00%> (?)
go-1.24 98.92% <100.00%> (?)
macos-latest 98.92% <100.00%> (-0.30%) ⬇️
ubuntu-latest 98.92% <100.00%> (-0.30%) ⬇️

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.

Copy link

@Copilot 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 streamlines path parsing by replacing byte-slice counting with direct string counting for a modest performance gain.

  • Refactored countParams to use strings.Count instead of bytesconv.StringToBytes + bytes.Count
  • Refactored countSections similarly
  • Removed unused bytes import and the related strColon, strStar, and strSlash variables
Comments suppressed due to low confidence (2)

tree.go:14

  • The bytesconv importer appears unused after removing all StringToBytes calls. Consider removing this import to clean up dependencies.
github.com/gin-gonic/gin/internal/bytesconv

tree.go:80

  • [nitpick] Consider adding unit tests for countParams (and similarly countSections) to cover edge cases like empty strings, no separators, or strings composed solely of separators to ensure behavior remains correct.
func countParams(path string) uint16 {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants