Skip to content

maps: Performance Regression in maps.Clone after Go 1.24 Refactor #72983

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

Closed
1911860538 opened this issue Mar 21, 2025 · 5 comments
Closed

maps: Performance Regression in maps.Clone after Go 1.24 Refactor #72983

1911860538 opened this issue Mar 21, 2025 · 5 comments
Labels
BugReport Issues describing a possible bug in the Go implementation.

Comments

@1911860538
Copy link

1911860538 commented Mar 21, 2025

Go 1.24 refactored the map runtime using SwissTable.

When I was using Go 1.24, I found that,
compared to using a for loop to copy a map,
the performance of the built-in maps.Clone was actually worse.
At the same time, in versions prior to Go 1.24,
the performance of maps.Clone was the best.

Below is the code I used for performance testing:

package clone

import (
	"maps"
	"testing"
)

func clone1(m map[string]string) map[string]string {
	if m == nil {
		return nil
	}
	m2 := make(map[string]string, len(m))
	for k, v := range m {
		m2[k] = v
	}
	return m2
}

func clone2[M ~map[K]V, K comparable, V any](m M) M {
	if m == nil {
		return nil
	}
	m2 := make(M, len(m))
	for k, v := range m {
		m2[k] = v
	}
	return m2
}

var m = map[string]string{
	"k1": "v1",
	"k2": "v2",
	"k3": "v3",
	"k4": "v4",
}

func BenchmarkClone1(b *testing.B) {
	for i := 0; i < b.N; i++ {
		clone1(m)
	}
}

func BenchmarkClone2(b *testing.B) {
	for i := 0; i < b.N; i++ {
		clone2(m)
	}
}

func BenchmarkStdClone(b *testing.B) {
	for i := 0; i < b.N; i++ {
		maps.Clone(m)
	}
}

Performance test results for different Go versions are as follows:

Go 1.24

goos: darwin
goarch: arm64
pkg: test_code/test_map/clone
cpu: Apple M3 Pro
BenchmarkClone1
BenchmarkClone1-11      	16550962	        72.64 ns/op
BenchmarkClone2
BenchmarkClone2-11      	16820404	        71.59 ns/op
BenchmarkStdClone
BenchmarkStdClone-11    	 9009817	       134.1 ns/op
PASS

Process finished with the exit code 0

Go 1.23

goos: darwin
goarch: arm64
pkg: test_code/test_map/clone
cpu: Apple M3 Pro
BenchmarkClone1
BenchmarkClone1-11      	15726506	        77.62 ns/op
BenchmarkClone2
BenchmarkClone2-11      	14940602	        80.03 ns/op
BenchmarkStdClone
BenchmarkStdClone-11    	20451465	        59.58 ns/op
PASS

Process finished with the exit code 0

The results for Go 1.22 and Go 1.21 are similar to Go 1.23.

@gabyhelp
Copy link

Related Issues

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

@gabyhelp gabyhelp added the BugReport Issues describing a possible bug in the Go implementation. label Mar 21, 2025
@seankhliao
Copy link
Member

That benchmark looks invalid, the return value is discarded and can be optimized away.
Ensuring the value is retained produces more consistent results: https://go.dev/play/p/jBtvROSBTzV

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Mar 21, 2025
@1911860538
Copy link
Author

That benchmark looks invalid, the return value is discarded and can be optimized away. Ensuring the value is retained produces more consistent results: https://go.dev/play/p/jBtvROSBTzV

I conducted benchmarks for Go 1.23 and Go 1.24 locally based on your code.
Below is the test code. Using the same test code, I tested it with Go 1.24 and Go 1.23 separately.
For the same maps.Clone code, the execution speed in Go 1.23 is twice that of Go 1.24.

clone_test.go

package clone2

import (
	"maps"
	"testing"
)

var g map[string]string

var m = map[string]string{
	"k1": "v1",
	"k2": "v2",
	"k3": "v3",
	"k4": "v4",
}

func BenchmarkStdClone(b *testing.B) {
	for i := 0; i < b.N; i++ {
		g = maps.Clone(m)
	}
}

Go1.24 output

GOROOT=/Users/huangzl/sdk/go1.24.0 #gosetup
GOPATH=/Users/huangzl/huangzhiwen/about_code/go/test_code/GOPATH #gosetup
GOPROXY=https://goproxy.cn,direct #gosetup
/Users/huangzl/sdk/go1.24.0/bin/go test -c -o /Users/huangzl/Library/Caches/JetBrains/GoLand2024.3/tmp/GoLand/___gobench_test_code_test_map_clone2.test test_code/test_map/clone2 #gosetup
/Users/huangzl/Library/Caches/JetBrains/GoLand2024.3/tmp/GoLand/___gobench_test_code_test_map_clone2.test -test.v -test.paniconexit0 -test.bench . -test.run ^$ #gosetup
goos: darwin
goarch: arm64
pkg: test_code/test_map/clone2
cpu: Apple M3 Pro
BenchmarkStdClone
BenchmarkStdClone-11    	 7470522	       137.7 ns/op
PASS

Process finished with the exit code 0

Go1.23 output

GOROOT=/Users/huangzl/sdk/go1.23.1 #gosetup
GOPATH=/Users/huangzl/huangzhiwen/about_code/go/test_code/GOPATH #gosetup
GOPROXY=https://goproxy.cn,direct #gosetup
/Users/huangzl/sdk/go1.23.1/bin/go test -c -o /Users/huangzl/Library/Caches/JetBrains/GoLand2024.3/tmp/GoLand/___gobench_test_code_test_map_clone2.test test_code/test_map/clone2 #gosetup
/Users/huangzl/Library/Caches/JetBrains/GoLand2024.3/tmp/GoLand/___gobench_test_code_test_map_clone2.test -test.v -test.paniconexit0 -test.bench . -test.run ^$ #gosetup
goos: darwin
goarch: arm64
pkg: test_code/test_map/clone2
cpu: Apple M3 Pro
BenchmarkStdClone
BenchmarkStdClone-11    	20376992	        58.62 ns/op
PASS

Process finished with the exit code 0

@mateusz834
Copy link
Member

AFAIAC in go1.24 the maps.Clone is not optimized specially as it was for non-swiss maps in 1.23

(#70836)

@1911860538
Copy link
Author

AFAIAC in go1.24 the maps.Clone is not optimized specially as it was for non-swiss maps in 1.23

(#70836)

So the performance regression of maps.Clone is a fact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BugReport Issues describing a possible bug in the Go implementation.
Projects
None yet
Development

No branches or pull requests

4 participants