From 71446e99512f6026078a653d1e61ed945dd166a9 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 4 Jul 2024 14:53:44 +1000 Subject: [PATCH] os: optimise cgo clearenv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For programs with very large environments, calling unsetenv(3) for each environment variable can be fairly 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 os.Clearenv() ~30% faster when run an environment with 1000 environment variable set (and gets better with more environment variables): cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ Env-16 213.6µ ± 1% 185.1µ ± 3% -13.32% (p=0.000 n=10) (The time taken to set up 1000 variables in this benchmark is 117.2µs, so while the naive timing is 13% the speedup for os.Clearenv() itself the speedup is from 96.4µs to 67.9µs which is ~30%.) Change-Id: I589794845854e1721e6fc5badc671474c2bbd037 --- src/runtime/cgo/clearenv.go | 15 +++++++++++++++ src/runtime/cgo/gcc_clearenv.c | 19 +++++++++++++++++++ src/runtime/runtime_clearenv.go | 29 +++++++++++++++++++++++++++++ src/runtime/runtime_noclearenv.go | 18 ++++++++++++++++++ src/syscall/env_unix.go | 5 ++--- src/syscall/syscall.go | 4 ++++ 6 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 src/runtime/cgo/clearenv.go create mode 100644 src/runtime/cgo/gcc_clearenv.c create mode 100644 src/runtime/runtime_clearenv.go create mode 100644 src/runtime/runtime_noclearenv.go diff --git a/src/runtime/cgo/clearenv.go b/src/runtime/cgo/clearenv.go new file mode 100644 index 0000000000000..6721abf19dd6b --- /dev/null +++ b/src/runtime/cgo/clearenv.go @@ -0,0 +1,15 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux + +package cgo + +import _ "unsafe" // for go:linkname + +//go:cgo_import_static x_cgo_clearenv +//go:linkname x_cgo_clearenv x_cgo_clearenv +//go:linkname _cgo_clearenv runtime._cgo_clearenv +var x_cgo_clearenv byte +var _cgo_clearenv = &x_cgo_clearenv diff --git a/src/runtime/cgo/gcc_clearenv.c b/src/runtime/cgo/gcc_clearenv.c new file mode 100644 index 0000000000000..244cbca0c2a94 --- /dev/null +++ b/src/runtime/cgo/gcc_clearenv.c @@ -0,0 +1,19 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux + +#include "libcgo.h" + +#include + +/* Stub for calling clearenv */ +void +x_cgo_clearenv(void **_unused) +{ + _cgo_tsan_acquire(); + clearenv(); + _cgo_tsan_release(); +} + diff --git a/src/runtime/runtime_clearenv.go b/src/runtime/runtime_clearenv.go new file mode 100644 index 0000000000000..ad49e61ab3d3b --- /dev/null +++ b/src/runtime/runtime_clearenv.go @@ -0,0 +1,29 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux + +package runtime + +import "unsafe" + +var _cgo_clearenv unsafe.Pointer // pointer to C function + +// Clear the C environment if cgo is loaded. +func clearenv_c() { + if _cgo_clearenv == nil { + return + } + asmcgocall(_cgo_clearenv, nil) +} + +//go:linkname syscall_runtimeClearenv syscall.runtimeClearenv +func syscall_runtimeClearenv(env map[string]int) { + clearenv_c() + // Did we just unset GODEBUG? + if _, ok := env["GODEBUG"]; ok { + godebugEnv.Store(nil) + godebugNotify(true) + } +} diff --git a/src/runtime/runtime_noclearenv.go b/src/runtime/runtime_noclearenv.go new file mode 100644 index 0000000000000..ea48e848a2d8f --- /dev/null +++ b/src/runtime/runtime_noclearenv.go @@ -0,0 +1,18 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !linux + +package runtime + +import _ "unsafe" // for go:linkname + +//go:linkname syscall_runtimeClearenv syscall.runtimeClearenv +func syscall_runtimeClearenv(env map[string]int) { + // The system doesn't have clearenv(3) so emulate it by unsetting all of + // the variables manually. + for k := range env { + syscall_runtimeUnsetenv(k) + } +} diff --git a/src/syscall/env_unix.go b/src/syscall/env_unix.go index 8e87e018e82de..4756e42653d88 100644 --- a/src/syscall/env_unix.go +++ b/src/syscall/env_unix.go @@ -129,9 +129,8 @@ func Clearenv() { envLock.Lock() defer envLock.Unlock() - for k := range env { - runtimeUnsetenv(k) - } + runtimeClearenv(env) + env = make(map[string]int) envs = []string{} } diff --git a/src/syscall/syscall.go b/src/syscall/syscall.go index a46f22ddb53c6..d70f4fbd15a53 100644 --- a/src/syscall/syscall.go +++ b/src/syscall/syscall.go @@ -104,3 +104,7 @@ func Exit(code int) // runtimeSetenv and runtimeUnsetenv are provided by the runtime. func runtimeSetenv(k, v string) func runtimeUnsetenv(k string) + +// runtimeClearenv is provided by the runtime (on platforms without +// clearenv(3), it is just a wrapper around runtimeUnsetenv). +func runtimeClearenv(env map[string]int)