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.