This is an automated email from the ASF dual-hosted git repository.
AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new e2eb0c451 perf(common): optimize IsEquals and ToMap to reduce
allocations (#3488)
e2eb0c451 is described below
commit e2eb0c451b9843c9a6f59f8598aea3ea4d7f4d50
Author: 超級の新人 <[email protected]>
AuthorDate: Sun Jul 19 16:02:09 2026 +0800
perf(common): optimize IsEquals and ToMap to reduce allocations (#3488)
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 single-pass
Location parsing with IndexByte.
Key improvements:
- ToMap: 38-57% faster, 35-51% less memory, 43-77% fewer allocations
- IsEquals: 40-76% faster, 50-100% less memory, 58-100% fewer allocations
- Removed duplicate Protocol assignment in ToMap
Fixes: #3394
Signed-off-by: chaojixinren <[email protected]>
---
common/constant/key.go | 3 +
common/url.go | 231 ++++++++++++++++++++++++++++++++++++++++---------
common/url_test.go | 116 +++++++++++++++++++++++++
3 files changed, 311 insertions(+), 39 deletions(-)
diff --git a/common/constant/key.go b/common/constant/key.go
index b8a89c43f..710457f71 100644
--- a/common/constant/key.go
+++ b/common/constant/key.go
@@ -52,6 +52,9 @@ const (
ReleaseKey = "release"
AnyhostKey = "anyhost"
PortKey = "port"
+ HostKey = "host"
+ UsernameKey = "username"
+ PasswordKey = "password"
ProtocolKey = "protocol"
PathSeparator = "/"
DotSeparator = "."
diff --git a/common/url.go b/common/url.go
index 5331ecf8c..0b21116e7 100644
--- a/common/url.go
+++ b/common/url.go
@@ -50,9 +50,14 @@ const (
CONFIGURATOR
ROUTER
PROVIDER
- PROTOCOL = "protocol"
)
+// PROTOCOL is the canonical key for the protocol in URL maps/params.
+//
+// Deprecated: use constant.ProtocolKey instead. Kept as an alias to preserve
+// backward compatibility for downstream users.
+const PROTOCOL = constant.ProtocolKey
+
var (
DubboNodes = [...]string{"consumers", "configurators",
"routers", "providers"} // Dubbo service node
DubboRole = [...]string{"consumer", "", "routers",
"provider"} // Dubbo service role
@@ -791,17 +796,18 @@ func (c *URL) GetParamAndDecoded(key string) (string,
error) {
// GetRawParam gets raw param
func (c *URL) GetRawParam(key string) string {
switch key {
- case PROTOCOL:
+ case constant.ProtocolKey:
return c.Protocol
- case "username":
+ case constant.UsernameKey:
return c.Username
- case "host":
- return strings.Split(c.Location, ":")[0]
- case "password":
+ case constant.HostKey:
+ host, _ := parseLocation(c.Location)
+ return host
+ case constant.PasswordKey:
return c.Password
- case "port":
+ case constant.PortKey:
return c.Port
- case "path":
+ case constant.PathKey:
return c.Path
default:
return c.GetParam(key, "")
@@ -909,7 +915,34 @@ func (c *URL) SetParams(m url.Values) {
// ToMap transfer URL to Map
func (c *URL) ToMap() map[string]string {
- paramsMap := make(map[string]string)
+ // Pre-calculate capacity to avoid map growth
+ c.paramsLock.RLock()
+ paramCount := len(c.params)
+ c.paramsLock.RUnlock()
+
+ // Count non-empty scalar fields
+ capacity := paramCount
+ if c.Protocol != "" {
+ capacity++
+ }
+ if c.Username != "" {
+ capacity++
+ }
+ if c.Password != "" {
+ capacity++
+ }
+ if c.Location != "" {
+ capacity += 2 // host + port
+ }
+ if c.Path != "" {
+ capacity++
+ }
+
+ if capacity == 0 {
+ return nil
+ }
+
+ paramsMap := make(map[string]string, capacity)
c.RangeParams(
func(key, value string) bool {
@@ -919,32 +952,21 @@ func (c *URL) ToMap() map[string]string {
)
if c.Protocol != "" {
- paramsMap[PROTOCOL] = c.Protocol
+ paramsMap[constant.ProtocolKey] = c.Protocol
}
if c.Username != "" {
- paramsMap["username"] = c.Username
+ paramsMap[constant.UsernameKey] = c.Username
}
if c.Password != "" {
- paramsMap["password"] = c.Password
+ paramsMap[constant.PasswordKey] = c.Password
}
if c.Location != "" {
- paramsMap["host"] = strings.Split(c.Location, ":")[0]
- var port string
- if strings.Contains(c.Location, ":") {
- port = strings.Split(c.Location, ":")[1]
- } else {
- port = "0"
- }
- paramsMap["port"] = port
- }
- if c.Protocol != "" {
- paramsMap[PROTOCOL] = c.Protocol
+ host, port := parseLocation(c.Location)
+ paramsMap[constant.HostKey] = host
+ paramsMap[constant.PortKey] = port
}
if c.Path != "" {
- paramsMap["path"] = c.Path
- }
- if len(paramsMap) == 0 {
- return nil
+ paramsMap[constant.PathKey] = c.Path
}
return paramsMap
}
@@ -1154,6 +1176,37 @@ func (c *URL) CloneWithParams(reserveParams []string)
*URL {
return c.CloneWithFilter(nil, reserveParams)
}
+// keySet is a set of URL parameter keys that should be ignored during
comparison.
+type keySet map[string]struct{}
+
+func newKeySet(keys []string) keySet {
+ set := make(keySet, len(keys))
+ for _, k := range keys {
+ set[k] = struct{}{}
+ }
+ return set
+}
+
+func (s keySet) contains(key string) bool {
+ _, ok := s[key]
+ return ok
+}
+
+// reservedKeyList are the keys that ToMap materializes from scalar fields (or
the
+// "host:port" Location), where a non-empty field overrides the same-named
param.
+var reservedKeyList = []string{
+ constant.ProtocolKey,
+ constant.UsernameKey,
+ constant.PasswordKey,
+ constant.HostKey,
+ constant.PortKey,
+ constant.PathKey,
+}
+
+// reservedKeys is reservedKeyList as a set. equalParams skips these keys so
that
+// equalReservedKeys can compare them with ToMap's field-overrides-param
precedence.
+var reservedKeys = newKeySet(reservedKeyList)
+
// 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) {
@@ -1163,28 +1216,128 @@ func IsEquals(left *URL, right *URL, excludes
...string) bool {
return false
}
- leftMap := left.ToMap()
- rightMap := right.ToMap()
- for _, exclude := range excludes {
- delete(leftMap, exclude)
- delete(rightMap, exclude)
+ excluded := newKeySet(excludes)
+ return equalReservedKeys(left, right, excluded) &&
+ equalParams(left, right, excluded)
+}
+
+// rawParam reports the first value of a param and whether the key is present
in
+// the params map (an empty-valued key still counts as present, matching
ToMap).
+func (c *URL) rawParam(key string) (string, bool) {
+ c.paramsLock.RLock()
+ defer c.paramsLock.RUnlock()
+
+ if len(c.params) == 0 {
+ return "", false
+ }
+ vs, ok := c.params[key]
+ if !ok || len(vs) == 0 {
+ return "", false
}
+ return vs[0], true
+}
- if len(leftMap) != len(rightMap) {
- return false
+// effectiveReserved returns the flattened value of a reserved key and whether
it
+// is present, matching ToMap's precedence: a non-empty scalar field (or a
+// non-empty Location for host/port) overrides the same-named param; otherwise
the
+// param, if any, is used.
+func effectiveReserved(u *URL, key string) (string, bool) {
+ switch key {
+ case constant.ProtocolKey:
+ if u.Protocol != "" {
+ return u.Protocol, true
+ }
+ case constant.UsernameKey:
+ if u.Username != "" {
+ return u.Username, true
+ }
+ case constant.PasswordKey:
+ if u.Password != "" {
+ return u.Password, true
+ }
+ case constant.PathKey:
+ if u.Path != "" {
+ return u.Path, true
+ }
+ case constant.HostKey:
+ if u.Location != "" {
+ host, _ := parseLocation(u.Location)
+ return host, true
+ }
+ case constant.PortKey:
+ if u.Location != "" {
+ _, port := parseLocation(u.Location)
+ return port, true
+ }
}
+ return u.rawParam(key)
+}
- for lk, lv := range leftMap {
- if rv, ok := rightMap[lk]; !ok {
- return false
- } else if lv != rv {
+// equalReservedKeys compares the reserved keys
(protocol/username/password/host/
+// port/path) of two URLs using ToMap precedence, skipping any excluded key.
+func equalReservedKeys(left, right *URL, excluded keySet) bool {
+ for _, key := range reservedKeyList {
+ if excluded.contains(key) {
+ continue
+ }
+ lv, lok := effectiveReserved(left, key)
+ rv, rok := effectiveReserved(right, key)
+ if lok != rok || lv != rv {
return false
}
}
-
return true
}
+// equalParams compares the non-reserved params of two URLs, skipping excluded
keys.
+// Reserved keys are handled by equalScalars/equalLocation, so they are
skipped here
+// to preserve ToMap's "field overrides same-named param" precedence. It avoids
+// materializing both maps by snapshotting only the left side and streaming
the right.
+func equalParams(left, right *URL, excluded keySet) bool {
+ leftParams := make(map[string]string)
+ left.RangeParams(func(key, value string) bool {
+ if !reservedKeys.contains(key) && !excluded.contains(key) {
+ leftParams[key] = value
+ }
+ return true
+ })
+
+ rightCount := 0
+ matched := true
+ right.RangeParams(func(key, value string) bool {
+ if reservedKeys.contains(key) || excluded.contains(key) {
+ return true
+ }
+ rightCount++
+ if lv, ok := leftParams[key]; !ok || lv != value {
+ matched = false
+ return false
+ }
+ return true
+ })
+
+ return matched && rightCount == len(leftParams)
+}
+
+// parseLocation splits "host:port" into host and port. The port is the segment
+// between the first and second ':' (equivalent to strings.Split(location,
":")[1]),
+// preserving the historical behavior for locations that contain extra ':'.
+func parseLocation(location string) (host, port string) {
+ if location == "" {
+ return "", "0"
+ }
+ idx := strings.IndexByte(location, ':')
+ if idx < 0 {
+ return location, "0"
+ }
+ host = location[:idx]
+ rest := location[idx+1:]
+ if j := strings.IndexByte(rest, ':'); j >= 0 {
+ return host, rest[:j]
+ }
+ return host, rest
+}
+
// URLSlice will be used to sort URL instance
// Instances will be order by URL.String()
type URLSlice []*URL
diff --git a/common/url_test.go b/common/url_test.go
index 5c37e9bfd..6df7e5f59 100644
--- a/common/url_test.go
+++ b/common/url_test.go
@@ -1168,6 +1168,122 @@ func TestIsEquals(t *testing.T) {
assert.False(t, IsEquals(u1, nil))
}
+// TestIsEqualsReservedKeyOverride guards the ToMap precedence semantics: a
+// non-empty scalar field overrides the same-named param, so two URLs whose
+// flattened maps are identical must be judged equal even if their raw params
differ.
+func TestIsEqualsReservedKeyOverride(t *testing.T) {
+ // Both have field Protocol "consumer" but different params["protocol"].
+ // After ToMap both flatten to protocol=consumer, so they are equal.
+ left, _ :=
NewURL("consumer://127.0.0.1:20000/com.test.Service?key1=value1")
+ right, _ :=
NewURL("consumer://127.0.0.1:20000/com.test.Service?key1=value1")
+ left.SetParam(constant.ProtocolKey, "tri")
+ right.SetParam(constant.ProtocolKey, "dubbo")
+
+ assert.True(t, IsEquals(left, right), "field protocol should override
param protocol")
+ // symmetry
+ assert.True(t, IsEquals(right, left))
+
+ // Excluding the reserved key keeps them equal too.
+ assert.True(t, IsEquals(left, right, constant.ProtocolKey))
+}
+
+// TestIsEqualsReservedKeyFallback verifies that when the scalar field is
empty,
+// the same-named param participates in the comparison (matching ToMap
fallback).
+func TestIsEqualsReservedKeyFallback(t *testing.T) {
+ left := &URL{Ip: "127.0.0.1", Port: "20000"}
+ right := &URL{Ip: "127.0.0.1", Port: "20000"}
+ left.SetParam(constant.PathKey, "/a")
+ right.SetParam(constant.PathKey, "/b")
+ // Path field empty on both -> fall back to params, which differ.
+ assert.False(t, IsEquals(left, right))
+
+ right.SetParam(constant.PathKey, "/a")
+ assert.True(t, IsEquals(left, right))
+
+ // Field on one side overrides its param and must match the other
side's field.
+ left.Path = "/a"
+ left.SetParam(constant.PathKey, "/ignored")
+ assert.True(t, IsEquals(left, right))
+}
+
+// TestIsEqualsReservedKeyPresence guards presence mismatch: one side has a
+// reserved param, the other has neither field nor param.
+func TestIsEqualsReservedKeyPresence(t *testing.T) {
+ left := &URL{Ip: "127.0.0.1", Port: "20000"}
+ right := &URL{Ip: "127.0.0.1", Port: "20000"}
+ left.SetParam(constant.UsernameKey, "alice")
+ assert.False(t, IsEquals(left, right))
+ assert.False(t, IsEquals(right, left))
+}
+
+// TestIsEqualsHostPortOverride guards ToMap precedence for host/port: a
non-empty
+// Location overrides same-named params, so differing params["host"]/["port"]
must
+// not make otherwise-identical URLs unequal.
+func TestIsEqualsHostPortOverride(t *testing.T) {
+ left, _ :=
NewURL("dubbo://127.0.0.1:20000/com.test.Service?key1=value1")
+ right, _ :=
NewURL("dubbo://127.0.0.1:20000/com.test.Service?key1=value1")
+ left.SetParam(constant.HostKey, "1.1.1.1")
+ right.SetParam(constant.HostKey, "2.2.2.2")
+ left.SetParam(constant.PortKey, "1")
+ right.SetParam(constant.PortKey, "2")
+
+ // Location (127.0.0.1:20000) overrides both host and port params on
each side.
+ assert.True(t, IsEquals(left, right), "Location should override
host/port params")
+ assert.True(t, IsEquals(right, left))
+}
+
+// TestIsEqualsHostPortFallbackAndExclude covers the host/port conflict +
exclude +
+// symmetry cases when Location is empty and the params provide host/port.
+func TestIsEqualsHostPortFallbackAndExclude(t *testing.T) {
+ left := &URL{Ip: "127.0.0.1", Port: "20000"}
+ right := &URL{Ip: "127.0.0.1", Port: "20000"}
+ left.SetParam(constant.HostKey, "a")
+ right.SetParam(constant.HostKey, "b")
+ left.SetParam(constant.PortKey, "1")
+ right.SetParam(constant.PortKey, "1")
+
+ // host param differs -> not equal
+ assert.False(t, IsEquals(left, right))
+ assert.False(t, IsEquals(right, left))
+
+ // exclude host -> equal (port still matches)
+ assert.True(t, IsEquals(left, right, constant.HostKey))
+ assert.True(t, IsEquals(right, left, constant.HostKey))
+
+ // now make port differ too, exclude both host and port -> equal
+ right.SetParam(constant.PortKey, "2")
+ assert.False(t, IsEquals(left, right, constant.HostKey))
+ assert.True(t, IsEquals(left, right, constant.HostKey,
constant.PortKey))
+}
+
+// TestParseLocationMultiColon guards that a location with extra ':' keeps the
+// historical port semantics (the segment between the first and second ':').
+func TestParseLocationMultiColon(t *testing.T) {
+ host, port := parseLocation("127.0.0.1:2181:extra")
+ assert.Equal(t, "127.0.0.1", host)
+ assert.Equal(t, "2181", port)
+
+ host, port = parseLocation("127.0.0.1:2181")
+ assert.Equal(t, "127.0.0.1", host)
+ assert.Equal(t, "2181", port)
+
+ host, port = parseLocation("127.0.0.1")
+ assert.Equal(t, "127.0.0.1", host)
+ assert.Equal(t, "0", port)
+
+ u := &URL{Location: "127.0.0.1:2181:2182"}
+ m := u.ToMap()
+ assert.Equal(t, "2181", m[constant.PortKey])
+ assert.Equal(t, "127.0.0.1", m[constant.HostKey])
+}
+
+// TestProtocolConstantAlias ensures the deprecated exported PROTOCOL alias is
+// preserved for backward compatibility.
+func TestProtocolConstantAlias(t *testing.T) {
+ assert.Equal(t, constant.ProtocolKey, PROTOCOL)
+ assert.Equal(t, "protocol", PROTOCOL)
+}
+
func TestGetSubscribeName(t *testing.T) {
u, _ :=
NewURL("dubbo://127.0.0.1:20000?interface=com.test.Service&version=1.0&group=test")
name := GetSubscribeName(u)