[go] database/sql: isolate hot atomic counters from connection-pool mutex

4 views
Skip to first unread message

Gido ten Cate (Gerrit)

unread,
Apr 15, 2026, 4:42:45 PMApr 15
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Gido ten Cate has uploaded the change for review

Commit message

database/sql: isolate hot atomic counters from connection-pool mutex

DB.waitDuration and DB.numClosed are updated atomically on every
completed query and connection close respectively. In the current layout
they share a 64-byte cache line with DB.mu and the connection pool
fields.

Any CPU that writes to waitDuration or numClosed transitions that cache
line from Shared to Modified (MESI), invalidating cached copies on all
other CPUs. A goroutine about to acquire mu must then refetch the line
before the CAS, adding a cache-miss penalty of 30-200 ns per lock
acquisition under concurrent load.

Add a padding field between numClosed and mu so that the two atomic
counters occupy cache line 0 and mu begins on cache line 1. Move
connector (set once in Open, never written after that) to the end of
the struct to keep the hot cache line free of cold data.

The padding size is computed as 64 - unsafe.Sizeof(atomic.Int64{}) -
unsafe.Sizeof(atomic.Uint64{}) so it adapts automatically if the atomic
types grow.

DB.connector is a driver.Connector interface (16 bytes); its position
change does not affect any exported API. Struct size increases by 48
bytes. sql.DB is typically a long-lived application-global; the overhead
is negligible.

Benchmarks (goos: linux, goarch: amd64, cpu: AMD Ryzen 7 7800X3D,
GOMAXPROCS=16):

name old time/op new time/op delta
DBMutex/stressors=1 83.6ns +- 1% 29.2ns +- 1% -65.1%
DBMutex/stressors=2 139.8ns +- 1% 28.6ns +- 1% -79.5%
DBMutex/stressors=4 246.0ns +- 8% 27.4ns +- 2% -88.9%

Fixes #78763
Change-Id: I2d7127b35f69d0349a6d151b9d84634a87e5d853

Change diff

diff --git a/src/database/sql/bench_layout_test.go b/src/database/sql/bench_layout_test.go
new file mode 100644
index 0000000..9391bf2
--- /dev/null
+++ b/src/database/sql/bench_layout_test.go
@@ -0,0 +1,63 @@
+// Copyright 2026 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.
+
+package sql
+
+// BenchmarkDBMutex measures the impact of cache-line false sharing
+// between DB.waitDuration/numClosed (hot atomic counters) and DB.mu
+// (the connection-pool mutex).
+//
+// N stressor goroutines continuously perform atomic adds to both counters
+// while the benchmark goroutines time the mu.Lock() / peek / mu.Unlock()
+// cycle. Without padding the stressor writes invalidate the cache line
+// holding mu on all other CPUs, forcing a re-fetch before each CAS.
+//
+// Run with:
+//
+// go test database/sql -bench=BenchmarkDBMutex -benchtime=5s -count=5
+import (
+ "fmt"
+ "testing"
+)
+
+func BenchmarkDBMutex(b *testing.B) {
+ for _, nStressors := range []int{0, 1, 2, 4} {
+ nStressors := nStressors
+ b.Run(fmt.Sprintf("stressors=%d", nStressors), func(b *testing.B) {
+ db := &DB{}
+
+ // Start stressor goroutines that continuously update the
+ // hot atomic counters, replicating the false-sharing pattern.
+ stop := make(chan struct{})
+ for range nStressors {
+ go func() {
+ for {
+ select {
+ case <-stop:
+ return
+ default:
+ db.waitDuration.Add(1)
+ db.numClosed.Add(1)
+ }
+ }
+ }()
+ }
+
+ b.ResetTimer()
+ for b.Loop() {
+ db.mu.Lock()
+ // Simulate a minimal pool peek — just read the length
+ // of freeConn, which is the first thing conn() does.
+ _ = len(db.freeConn)
+ db.mu.Unlock()
+ }
+ b.StopTimer()
+
+ close(stop)
+
+ // Prevent the compiler from optimising away the atomic ops.
+ _ = db.waitDuration.Load()
+ })
+ }
+}
diff --git a/src/database/sql/sql.go b/src/database/sql/sql.go
index 6b7d073..902e1c6 100644
--- a/src/database/sql/sql.go
+++ b/src/database/sql/sql.go
@@ -30,7 +30,7 @@
"sync"
"sync/atomic"
"time"
- _ "unsafe"
+ "unsafe"
)

var driversMu sync.RWMutex
@@ -509,14 +509,25 @@
// connection is returned to [DB]'s idle connection pool. The pool size
// can be controlled with [DB.SetMaxIdleConns].
type DB struct {
+ // waitDuration and numClosed are updated atomically on every query and
+ // connection close. They must occupy their own cache line; sharing a
+ // line with mu means every atomic store forces all CPUs holding mu to
+ // re-fetch the line (MESI Modified→Shared), adding ~30–200 ns per lock
+ // acquisition under concurrent load depending on core count.
+ //
+ // connector is cold (set once in Open, never written afterwards) and is
+ // placed after the mutex-protected section to keep the hot cache line
+ // free of cold data.
+
// Total time waited for new connections.
waitDuration atomic.Int64
-
- connector driver.Connector
// numClosed is an atomic counter which represents a total number of
// closed connections. Stmt.openStmt checks it before cleaning closed
// connections in Stmt.css.
numClosed atomic.Uint64
+ // Pad to the end of the cache line so that mu and the fields it
+ // protects do not share a cache line with the hot atomics above.
+ _ [64 - unsafe.Sizeof(atomic.Int64{}) - unsafe.Sizeof(atomic.Uint64{})]byte

mu sync.Mutex // protects following fields
freeConn []*driverConn // free connections ordered by returnedAt oldest to newest
@@ -542,6 +553,11 @@
maxLifetimeClosed int64 // Total number of connections closed due to max connection lifetime limit.

stop func() // stop cancels the connection opener.
+
+ // connector is set once in Open and never written afterwards.
+ // Keeping it here (off the hot cache line) avoids polluting the
+ // line shared by the hot atomics with a cold pointer.
+ connector driver.Connector
}

// connReuseStrategy determines how (*DB).conn returns database connections.

Change information

Files:
  • A src/database/sql/bench_layout_test.go
  • M src/database/sql/sql.go
Change size: M
Delta: 2 files changed, 82 insertions(+), 3 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I2d7127b35f69d0349a6d151b9d84634a87e5d853
Gerrit-Change-Number: 767580
Gerrit-PatchSet: 1
Gerrit-Owner: Gido ten Cate <engi...@gmail.com>
unsatisfied_requirement
satisfied_requirement
open
diffy

Gido ten Cate (Gerrit)

unread,
Apr 15, 2026, 4:55:59 PMApr 15
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
Attention needed from Brad Fitzpatrick and Daniel Theophanes

Gido ten Cate uploaded new patchset

Gido ten Cate uploaded patch set #2 to this change.
Open in Gerrit

Related details

Attention is currently required from:
  • Brad Fitzpatrick
  • Daniel Theophanes
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newpatchset
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I2d7127b35f69d0349a6d151b9d84634a87e5d853
Gerrit-Change-Number: 767580
Gerrit-PatchSet: 2
Gerrit-Owner: Gido ten Cate <engi...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Daniel Theophanes <kard...@gmail.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Kevin Burke <ke...@burke.dev>
Gerrit-Attention: Daniel Theophanes <kard...@gmail.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
unsatisfied_requirement
satisfied_requirement
open
diffy

Gido ten Cate (Gerrit)

unread,
Apr 16, 2026, 12:43:37 PMApr 16
to goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Daniel Theophanes, Kevin Burke, Gopher Robot, golang-co...@googlegroups.com
Attention needed from Brad Fitzpatrick and Daniel Theophanes

Message from Gido ten Cate

Adding benchmark data comparing master and this CL under concurrent atomic write load:

  BenchmarkDBMutex/stressors=0   master: 3.44 ns/op   fixed: 3.44 ns/op   (baseline, no contention)
BenchmarkDBMutex/stressors=1 master: 27.4 ns/op fixed: 3.5 ns/op (7.7× improvement)
BenchmarkDBMutex/stressors=2 master: 46.6 ns/op fixed: 3.4 ns/op (13.8× improvement)
BenchmarkDBMutex/stressors=4 master: 85.8 ns/op fixed: 3.4 ns/op (25× improvement)

Stressor goroutines call waitDuration.Add/numClosed.Add continuously to simulate concurrent atomic writes invalidating the cache line that mu sits on.

To be precise about when this matters: the effect is zero under normal load where the connection pool has spare capacity and the atomics fire rarely. It materialises under pool exhaustion, where multiple goroutines are simultaneously waiting for connections (writing waitDuration) while others are acquiring mu. That is the high-contention scenario where database latency is hardest to reduce by other means.

Open in Gerrit

Related details

Attention is currently required from:
  • Brad Fitzpatrick
  • Daniel Theophanes
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I2d7127b35f69d0349a6d151b9d84634a87e5d853
Gerrit-Change-Number: 767580
Gerrit-PatchSet: 2
Gerrit-Owner: Gido ten Cate <engi...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Daniel Theophanes <kard...@gmail.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Kevin Burke <ke...@burke.dev>
Gerrit-Attention: Daniel Theophanes <kard...@gmail.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Comment-Date: Thu, 16 Apr 2026 16:43:28 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: No
unsatisfied_requirement
satisfied_requirement
open
diffy

Sean Liao (Gerrit)

unread,
6:21 PM (5 hours ago) 6:21 PM
to Gido ten Cate, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Daniel Theophanes, Kevin Burke, Gopher Robot, golang-co...@googlegroups.com
Attention needed from Brad Fitzpatrick, Daniel Theophanes and Gido ten Cate

Sean Liao voted and added 1 comment

Votes added by Sean Liao

Commit-Queue+1

1 comment

Commit Message
Line 40, Patchset 2 (Latest): DBMutex/stressors=4 246.0ns +- 8% 27.4ns +- 2% -88.9%
Open in Gerrit

Related details

Attention is currently required from:
  • Brad Fitzpatrick
  • Daniel Theophanes
  • Gido ten Cate
Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: go
    Gerrit-Branch: master
    Gerrit-Change-Id: I2d7127b35f69d0349a6d151b9d84634a87e5d853
    Gerrit-Change-Number: 767580
    Gerrit-PatchSet: 2
    Gerrit-Owner: Gido ten Cate <engi...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Daniel Theophanes <kard...@gmail.com>
    Gerrit-Reviewer: Sean Liao <se...@liao.dev>
    Gerrit-CC: Gopher Robot <go...@golang.org>
    Gerrit-CC: Kevin Burke <ke...@burke.dev>
    Gerrit-Attention: Gido ten Cate <engi...@gmail.com>
    Gerrit-Attention: Daniel Theophanes <kard...@gmail.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Comment-Date: Fri, 17 Jul 2026 22:21:10 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: Yes
    unsatisfied_requirement
    open
    diffy
    Reply all
    Reply to author
    Forward
    0 new messages