Skip to content

Commit 836fa74

Browse files
cypharrandall77
authored andcommitted
syscall: optimise cgo clearenv
For programs with very large environments, calling unsetenv(3) for each environment variable can be very expensive because of CGo overhead, but clearenv(3) is much faster. The only thing we have to track is whether GODEBUG is being unset by the operation, which can be done very quickly without resorting to doing unsetenv(3) for every variable. This change makes syscall.Clearenv() >98% faster when run in an environment with as little as 100 environment variables. (Note that due to golang#27217, it is necessary to modify BenchmarkClearenv to use t.StopTimer() and -benchtime=100x in order to get these benchmark times -- otherwise syscall.Setenv() time is included and the benchmarks give a more pessimistic 50% performance improvement.) goos: linux goarch: amd64 pkg: syscall cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics │ before │ after │ │ sec/op │ sec/op vs base │ Clearenv/100-16 22276.5n ± 5% 285.8n ± 3% -98.72% (p=0.000 n=10) Clearenv/1000-16 1414104.0n ± 1% 783.1n ± 8% -99.94% (p=0.000 n=10) Clearenv/10000-16 143827.554µ ± 1% 7.591µ ± 5% -99.99% (p=0.000 n=10) geomean 1.655m 1.193µ -99.93% The above benchmarks are CGo builds, which require CGo overhead for every setenv(2). If you run the same benchmarks for a non-CGo package (i.e., outside of the "syscall" package), you get slightly more modest performance improvements: goos: linux goarch: amd64 pkg: clearenv_nocgo cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics │ before │ after │ │ sec/op │ sec/op vs base │ Clearenv/100-16 1106.0n ± 3% 230.7n ± 8% -79.14% (p=0.000 n=10) Clearenv/1000-16 11222.0n ± 1% 305.4n ± 6% -97.28% (p=0.000 n=10) Clearenv/10000-16 195676.5n ± 6% 759.9n ± 10% -99.61% (p=0.000 n=10) geomean 13.44µ 376.9n -97.20% (As above, this requires modifying the benchmarks to use t.StopTimer() and -benchtime=100x.) Change-Id: I53b96a75f189e91affbde423c907888b7e0fafcd GitHub-Last-Rev: f8d7a81 GitHub-Pull-Request: golang#70672 Reviewed-on: https://go-review.googlesource.com/c/go/+/633515 Reviewed-by: Mark Freeman <[email protected]> Reviewed-by: Keith Randall <[email protected]> Reviewed-by: Kirill Kolyshkin <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent ce39174 commit 836fa74

File tree

7 files changed

+231
-3
lines changed

7 files changed

+231
-3
lines changed

src/runtime/cgo/clearenv.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build linux
6+
7+
package cgo
8+
9+
import _ "unsafe" // for go:linkname
10+
11+
//go:cgo_import_static x_cgo_clearenv
12+
//go:linkname x_cgo_clearenv x_cgo_clearenv
13+
//go:linkname _cgo_clearenv runtime._cgo_clearenv
14+
var x_cgo_clearenv byte
15+
var _cgo_clearenv = &x_cgo_clearenv

src/runtime/cgo/gcc_clearenv.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build linux
6+
7+
#include "libcgo.h"
8+
9+
#include <stdlib.h>
10+
11+
/* Stub for calling clearenv */
12+
void
13+
x_cgo_clearenv(void **_unused)
14+
{
15+
_cgo_tsan_acquire();
16+
clearenv();
17+
_cgo_tsan_release();
18+
}

src/runtime/runtime_clearenv.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build linux
6+
7+
package runtime
8+
9+
import "unsafe"
10+
11+
var _cgo_clearenv unsafe.Pointer // pointer to C function
12+
13+
// Clear the C environment if cgo is loaded.
14+
func clearenv_c() {
15+
if _cgo_clearenv == nil {
16+
return
17+
}
18+
asmcgocall(_cgo_clearenv, nil)
19+
}
20+
21+
//go:linkname syscall_runtimeClearenv syscall.runtimeClearenv
22+
func syscall_runtimeClearenv(env map[string]int) {
23+
clearenv_c()
24+
// Did we just unset GODEBUG?
25+
if _, ok := env["GODEBUG"]; ok {
26+
godebugEnv.Store(nil)
27+
godebugNotify(true)
28+
}
29+
}

src/runtime/runtime_noclearenv.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !linux
6+
7+
package runtime
8+
9+
import _ "unsafe" // for go:linkname
10+
11+
//go:linkname syscall_runtimeClearenv syscall.runtimeClearenv
12+
func syscall_runtimeClearenv(env map[string]int) {
13+
// The system doesn't have clearenv(3) so emulate it by unsetting all of
14+
// the variables manually.
15+
for k := range env {
16+
syscall_runtimeUnsetenv(k)
17+
}
18+
}

src/syscall/env_unix.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,8 @@ func Clearenv() {
126126
envLock.Lock()
127127
defer envLock.Unlock()
128128

129-
for k := range env {
130-
runtimeUnsetenv(k)
131-
}
129+
runtimeClearenv(env)
130+
132131
env = make(map[string]int)
133132
envs = []string{}
134133
}

src/syscall/env_unix_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build unix || (js && wasm) || plan9 || wasip1
6+
7+
package syscall_test
8+
9+
import (
10+
"fmt"
11+
"strconv"
12+
"strings"
13+
"syscall"
14+
"testing"
15+
)
16+
17+
type env struct {
18+
name, val string
19+
}
20+
21+
func genDummyEnv(tb testing.TB, size int) []env {
22+
tb.Helper()
23+
envList := make([]env, size)
24+
for idx := range size {
25+
envList[idx] = env{
26+
name: fmt.Sprintf("DUMMY_VAR_%d", idx),
27+
val: fmt.Sprintf("val-%d", idx*100),
28+
}
29+
}
30+
return envList
31+
}
32+
33+
func setDummyEnv(tb testing.TB, envList []env) {
34+
tb.Helper()
35+
for _, env := range envList {
36+
if err := syscall.Setenv(env.name, env.val); err != nil {
37+
tb.Fatalf("setenv %s=%q failed: %v", env.name, env.val, err)
38+
}
39+
}
40+
}
41+
42+
func setupEnvCleanup(tb testing.TB) {
43+
tb.Helper()
44+
originalEnv := map[string]string{}
45+
for _, env := range syscall.Environ() {
46+
fields := strings.SplitN(env, "=", 2)
47+
name, val := fields[0], fields[1]
48+
originalEnv[name] = val
49+
}
50+
tb.Cleanup(func() {
51+
syscall.Clearenv()
52+
for name, val := range originalEnv {
53+
if err := syscall.Setenv(name, val); err != nil {
54+
tb.Fatalf("could not reset env %s=%q: %v", name, val, err)
55+
}
56+
}
57+
})
58+
}
59+
60+
func TestClearenv(t *testing.T) {
61+
setupEnvCleanup(t)
62+
63+
t.Run("DummyVars-4096", func(t *testing.T) {
64+
envList := genDummyEnv(t, 4096)
65+
setDummyEnv(t, envList)
66+
67+
if env := syscall.Environ(); len(env) < 4096 {
68+
t.Fatalf("env is missing dummy variables: %v", env)
69+
}
70+
for idx := range 4096 {
71+
name := fmt.Sprintf("DUMMY_VAR_%d", idx)
72+
if _, ok := syscall.Getenv(name); !ok {
73+
t.Fatalf("env is missing dummy variable %s", name)
74+
}
75+
}
76+
77+
syscall.Clearenv()
78+
79+
if env := syscall.Environ(); len(env) != 0 {
80+
t.Fatalf("clearenv should've cleared all variables: %v still set", env)
81+
}
82+
for idx := range 4096 {
83+
name := fmt.Sprintf("DUMMY_VAR_%d", idx)
84+
if val, ok := syscall.Getenv(name); ok {
85+
t.Fatalf("clearenv should've cleared all variables: %s=%q still set", name, val)
86+
}
87+
}
88+
})
89+
90+
// Test that GODEBUG getting cleared by Clearenv also resets the behaviour.
91+
t.Run("GODEBUG", func(t *testing.T) {
92+
envList := genDummyEnv(t, 100)
93+
setDummyEnv(t, envList)
94+
95+
doNilPanic := func() (ret any) {
96+
defer func() {
97+
ret = recover()
98+
}()
99+
panic(nil)
100+
return "should not return"
101+
}
102+
103+
// Allow panic(nil).
104+
if err := syscall.Setenv("GODEBUG", "panicnil=1"); err != nil {
105+
t.Fatalf("setenv GODEBUG=panicnil=1 failed: %v", err)
106+
}
107+
108+
got := doNilPanic()
109+
if got != nil {
110+
t.Fatalf("GODEBUG=panicnil=1 did not allow for nil panic: got %#v", got)
111+
}
112+
113+
// Disallow panic(nil).
114+
syscall.Clearenv()
115+
116+
if env := syscall.Environ(); len(env) != 0 {
117+
t.Fatalf("clearenv should've cleared all variables: %v still set", env)
118+
}
119+
120+
got = doNilPanic()
121+
if got == nil {
122+
t.Fatalf("GODEBUG=panicnil=1 being unset didn't reset panicnil behaviour")
123+
}
124+
if godebug, ok := syscall.Getenv("GODEBUG"); ok {
125+
t.Fatalf("GODEBUG still exists in environment despite being unset: GODEBUG=%q", godebug)
126+
}
127+
})
128+
}
129+
130+
func BenchmarkClearenv(b *testing.B) {
131+
setupEnvCleanup(b)
132+
b.ResetTimer()
133+
for _, size := range []int{100, 1000, 10000} {
134+
b.Run(strconv.Itoa(size), func(b *testing.B) {
135+
envList := genDummyEnv(b, size)
136+
for b.Loop() {
137+
// Ideally we would use b.StopTimer() for the setDummyEnv
138+
// portion, but this causes the benchmark time to get confused
139+
// and take forever. See <https://go.dev/issue/27217>.
140+
setDummyEnv(b, envList)
141+
syscall.Clearenv()
142+
}
143+
})
144+
}
145+
}

src/syscall/syscall.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,7 @@ func Exit(code int)
104104
// runtimeSetenv and runtimeUnsetenv are provided by the runtime.
105105
func runtimeSetenv(k, v string)
106106
func runtimeUnsetenv(k string)
107+
108+
// runtimeClearenv is provided by the runtime (on platforms without
109+
// clearenv(3), it is just a wrapper around runtimeUnsetenv).
110+
func runtimeClearenv(env map[string]int)

0 commit comments

Comments
 (0)