perf: optimize IsEquals and ToMap to reduce allocations#3402
Conversation
Optimize URL equality comparison and map conversion to avoid unnecessary allocations. IsEquals() now compares fields directly instead of materializing two full maps via ToMap(). ToMap() pre-sizes the map and uses a single-pass split for Location parsing. Fixes: apache#3248 Signed-off-by: 温俊朗 <wenjunlang@wenjunlangdeMacBook-Air.local>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3402 +/- ##
==========================================
+ Coverage 52.40% 52.42% +0.01%
==========================================
Files 492 492
Lines 37785 37826 +41
==========================================
+ Hits 19800 19829 +29
- Misses 16380 16387 +7
- Partials 1605 1610 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
the target branch should be develop |
|
@ywxzm03 help review this :) |
|
update to latest develop and run https://github.com/apache/dubbo-go/blob/develop/common/url_benchmark_test.go to see the performance shift before and after you commit, and paste the result comparison here 可以参考 #3403 的pr body写法 |
| if left.Location != right.Location { | ||
| if isExcluded("host") && isExcluded("port") { | ||
| // Both excluded, skip Location comparison | ||
| } else if isExcluded("host") || isExcluded("port") { | ||
| // Only one side of host:port excluded — fall through to param comparison | ||
| } else { | ||
| return false | ||
| } else if lv != rv { | ||
| } | ||
| } |
There was a problem hiding this comment.
IsEquals 在只 exclude host 或 port 时会漏比较另一半 location
PR 里当 left.Location != right.Location 且只排除 host 或 port 其中一个时,代码直接“fall through”,但后面只比较 params,不再比较未被排除的 host/port。
你试一下Location: 10.0.0.1:20880 vs 10.0.0.2:20880,exclude port 后 PR 返回 true,base 返回 false。
| paramsMap := make(map[string]string) | ||
| c.paramsLock.RLock() | ||
| paramCount := len(c.params) | ||
| c.paramsLock.RUnlock() | ||
|
|
||
| fixedCount := 0 | ||
| if c.Protocol != "" { | ||
| fixedCount++ | ||
| } | ||
| if c.Username != "" { | ||
| fixedCount++ | ||
| } | ||
| if c.Password != "" { | ||
| fixedCount++ | ||
| } | ||
| if c.Location != "" { | ||
| fixedCount += 2 // host + port | ||
| } | ||
| if c.Path != "" { | ||
| fixedCount++ | ||
| } | ||
|
|
||
| if paramCount == 0 && fixedCount == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| paramsMap := make(map[string]string, paramCount+fixedCount) |
There was a problem hiding this comment.
paramCount单独加锁读一次,后面RangeParams又加锁遍历一次,多了一次锁操作。- 这不是一致性快照:两次锁之间 params 可能变,容量估算可能不准。
fixedCount只是容量估算,不影响逻辑;估多了也只是多分一点内存。- 如果 params 里本来就有
protocol、host、port这些 key,后面的固定字段会覆盖它们,fixedCount仍然按新增 key 算,容量也会估大。 - 性能收益主要来自预分配,但可读性代价有点高。
建议就是保留容量估算,去掉无意义初始化,并确保 protocol/location 行为不变。
| if c.Location != "" { | ||
| paramsMap["host"] = strings.Split(c.Location, ":")[0] | ||
| var port string | ||
| if strings.Contains(c.Location, ":") { | ||
| port = strings.Split(c.Location, ":")[1] | ||
| // Single-pass split avoids repeated string scanning. | ||
| parts := strings.Split(c.Location, ":") | ||
| paramsMap["host"] = parts[0] | ||
| if len(parts) > 1 { | ||
| paramsMap["port"] = parts[len(parts)-1] | ||
| } else { | ||
| port = "0" | ||
| paramsMap["port"] = "0" | ||
| } |
There was a problem hiding this comment.
ToMap 的多地址 port 解析语义被改了
原逻辑对 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183 的 port 是 2181,127.0.0.1 确实不合理,
PR 改成取最后一个 : 后的 2183。也不合理
这里改成合理的逻辑,然后在pr简介里面说明
| // --- Benchmarks for IsEquals and ToMap --- | ||
|
|
||
| func BenchmarkIsEquals_SmallParams(b *testing.B) { | ||
| u1, _ := NewURL("dubbo://127.0.0.1:20000/com.test.Service?key1=value1&key2=value2") | ||
| u2, _ := NewURL("dubbo://127.0.0.1:20000/com.test.Service?key1=value1&key2=value2") | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| IsEquals(u1, u2) | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkIsEquals_LargeParams(b *testing.B) { | ||
| base1 := "dubbo://127.0.0.1:20000/com.test.Service?" | ||
| base2 := "dubbo://127.0.0.1:20000/com.test.Service?" | ||
| for i := 0; i < 50; i++ { | ||
| kv := "key" + strconv.Itoa(i) + "=val" + strconv.Itoa(i) | ||
| if i > 0 { | ||
| base1 += "&" | ||
| base2 += "&" | ||
| } | ||
| base1 += kv | ||
| base2 += kv | ||
| } | ||
| u1, _ := NewURL(base1) | ||
| u2, _ := NewURL(base2) | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| IsEquals(u1, u2) | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkIsEquals_WithExcludes(b *testing.B) { | ||
| u1, _ := NewURL("dubbo://127.0.0.1:20000/com.test.Service?key1=value1&key2=different&key3=value3") | ||
| u2, _ := NewURL("dubbo://127.0.0.1:20000/com.test.Service?key1=value1&key2=other&key3=value3") | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| IsEquals(u1, u2, "key2") | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkToMap_SmallParams(b *testing.B) { | ||
| u, _ := NewURL("dubbo://127.0.0.1:20000/com.test.Service?key1=value1&key2=value2") | ||
| u.Username = "user" | ||
| u.Password = "pass" | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| _ = u.ToMap() | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkToMap_LargeParams(b *testing.B) { | ||
| base := "dubbo://127.0.0.1:20000/com.test.Service?" | ||
| for i := 0; i < 50; i++ { | ||
| if i > 0 { | ||
| base += "&" | ||
| } | ||
| base += "key" + strconv.Itoa(i) + "=val" + strconv.Itoa(i) | ||
| } | ||
| u, _ := NewURL(base) | ||
| u.Username = "user" | ||
| u.Password = "pass" | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| _ = u.ToMap() | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkToMap_EmptyParams(b *testing.B) { | ||
| u := &URL{ | ||
| Protocol: "dubbo", | ||
| Location: "127.0.0.1:20000", | ||
| Path: "/com.test.Service", | ||
| } | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| _ = u.ToMap() | ||
| } | ||
| } |
There was a problem hiding this comment.
不用自己写benchmark 直接复用已有的benchmark 或者在benchmark文件里面添加你需要的benchmark
| // IsEquals compares if two URLs equals with each other. Excludes are all parameter keys which should ignored. | ||
| func IsEquals(left *URL, right *URL, excludes ...string) bool { | ||
| if (left == nil && right != nil) || (right == nil && left != nil) { | ||
| return false | ||
| } | ||
| if left.Ip != right.Ip || left.Port != right.Port { | ||
| return false | ||
| } | ||
|
|
||
| leftMap := left.ToMap() | ||
| rightMap := right.ToMap() | ||
| for _, exclude := range excludes { | ||
| delete(leftMap, exclude) | ||
| delete(rightMap, exclude) | ||
| // Build a small lookup set for excluded keys to avoid repeated linear scans | ||
| // and to avoid materializing two full maps just for comparison. | ||
| var excludeSet map[string]struct{} | ||
| if len(excludes) > 0 { | ||
| excludeSet = make(map[string]struct{}, len(excludes)) | ||
| for _, k := range excludes { | ||
| excludeSet[k] = struct{}{} | ||
| } | ||
| } | ||
|
|
||
| if len(leftMap) != len(rightMap) { | ||
| return false | ||
| isExcluded := func(key string) bool { | ||
| if excludeSet == nil { | ||
| return false | ||
| } | ||
| _, ok := excludeSet[key] | ||
| return ok | ||
| } | ||
|
|
There was a problem hiding this comment.
This rewrite seems more complicated than necessary, and it also changes the original comparison semantics.
The old implementation compares the result of ToMap(), so excluding only "port" should still compare "host", and excluding only "host" should still compare "port". In the new logic, when left.Location != right.Location and only one of host/port is excluded, the code falls through to param comparison, but it never compares the remaining non-excluded part of Location.
For example, these should not be equal when only "port" is excluded:
left.Location = "10.0.0.1:20880"
right.Location = "10.0.0.2:20880"
IsEquals(left, right, "port")
Besides that, the code still materializes leftParams, so the optimization is not as simple as "avoid materializing maps"; it only avoids building two full ToMap() results. I suggest either keeping the original map-based comparison for correctness, or splitting host/port explicitly and comparing only the non-excluded fields.
|
|
||
| // Compare params directly without materializing maps. | ||
| // Build left params (excluding filtered keys), then verify right matches. | ||
| leftParams := make(map[string]string) |
There was a problem hiding this comment.
这里可以做个容量预估,len(left.params) 就有了
There was a problem hiding this comment.
Pull request overview
This PR targets allocation reductions in the common.URL utilities by optimizing equality checks and map conversion, aligning with issue #3394’s request to avoid materializing maps during IsEquals() and to tighten ToMap().
Changes:
- Reworks
IsEquals()to compare URL scalar fields + params directly, avoidingToMap()allocations during comparisons. - Optimizes
ToMap()by pre-sizing the result map and simplifyingLocationparsing. - Adds benchmarks for
IsEquals()andToMap()across small/large param sets and with excludes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| common/url.go | Optimizes ToMap() allocations and refactors IsEquals() to avoid map materialization. |
| common/url_test.go | Adds benchmarks for IsEquals() and ToMap() to validate allocation improvements. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| c.paramsLock.RLock() | ||
| paramCount := len(c.params) | ||
| c.paramsLock.RUnlock() | ||
|
|
||
| fixedCount := 0 | ||
| if c.Protocol != "" { | ||
| fixedCount++ | ||
| } | ||
| if c.Username != "" { | ||
| fixedCount++ | ||
| } | ||
| if c.Password != "" { | ||
| fixedCount++ | ||
| } | ||
| if c.Location != "" { | ||
| fixedCount += 2 // host + port | ||
| } | ||
| if c.Path != "" { | ||
| fixedCount++ | ||
| } | ||
|
|
||
| if paramCount == 0 && fixedCount == 0 { | ||
| return nil | ||
| } |
| // Single-pass split avoids repeated string scanning. | ||
| parts := strings.Split(c.Location, ":") | ||
| paramsMap["host"] = parts[0] | ||
| if len(parts) > 1 { | ||
| paramsMap["port"] = parts[len(parts)-1] |
| if left.Location != right.Location { | ||
| if isExcluded("host") && isExcluded("port") { | ||
| // Both excluded, skip Location comparison | ||
| } else if isExcluded("host") || isExcluded("port") { | ||
| // Only one side of host:port excluded — fall through to param comparison |
| if left.Path != right.Path && !isExcluded("path") { | ||
| return false | ||
| } | ||
| if left.Location != right.Location { |
There was a problem hiding this comment.
[P1] 这里把 host / port 的排除逻辑挂在整条 Location 上了:只传 host 或只传 port 时,left.Location != right.Location 也会直接落入“放行”分支,另一半地址差异被一并忽略。旧实现是按 ToMap() 删除单个 key 比较,两个字段的排除语义是独立的。请分别比较 host/port,或保留原来的按键删除语义。



Optimize URL equality comparison and map conversion to avoid unnecessary allocations. IsEquals() now compares fields directly instead of materializing two full maps via ToMap(). ToMap() pre-sizes the map and uses a single-pass split for Location parsing.
Fixes: #3394
Signed-off-by: 13927699266@163.com