Skip to content

Commit 82e0d08

Browse files
dsnetjohanbrandhorst
authored andcommitted
log: reduce lock contention
Logger.Log methods are called in a highly concurrent manner in many servers. Acquiring a lock in each method call results in high lock contention, especially since each lock covers a non-trivial amount of work (e.g., formatting the header and outputting to the writer). Changes made: * Modify the Logger to use atomics so that the header formatting can be moved out of the critical lock section. Acquiring the flags does not occur in the same critical section as outputting to the underlying writer, so this introduces a very slight consistency instability where concurrently calling multiple Logger.Output along with Logger.SetFlags may cause the older flags to be used by some ongoing Logger.Output calls even after Logger.SetFlags has returned. * Use a sync.Pool to buffer the intermediate buffer. This approach is identical to how fmt does it, with the same max cap mitigation for golang#23199. * Only protect outputting to the underlying writer with a lock to ensure serialized ordering of output. Performance: name old time/op new time/op delta Concurrent-24 19.9µs ± 2% 8.3µs ± 1% -58.37% (p=0.000 n=10+10) Updates golang#19438 Change-Id: I091beb7431d8661976a6c01cdb0d145e37fe3d22 Reviewed-on: https://go-review.googlesource.com/c/go/+/464344 TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Joseph Tsai <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Bryan Mills <[email protected]>
1 parent e8ad709 commit 82e0d08

File tree

2 files changed

+104
-51
lines changed

2 files changed

+104
-51
lines changed

src/log/log.go

Lines changed: 71 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ const (
5454
// the Writer's Write method. A Logger can be used simultaneously from
5555
// multiple goroutines; it guarantees to serialize access to the Writer.
5656
type Logger struct {
57-
mu sync.Mutex // ensures atomic writes; protects the following fields
58-
prefix string // prefix on each line to identify the logger (but see Lmsgprefix)
59-
flag int // properties
60-
out io.Writer // destination for output
61-
buf []byte // for accumulating text to write
62-
isDiscard atomic.Bool // whether out == io.Discard
57+
outMu sync.Mutex
58+
out io.Writer // destination for output
59+
60+
prefix atomic.Pointer[string] // prefix on each line to identify the logger (but see Lmsgprefix)
61+
flag atomic.Int32 // properties
62+
isDiscard atomic.Bool
6363
}
6464

6565
// New creates a new Logger. The out variable sets the
@@ -68,17 +68,17 @@ type Logger struct {
6868
// after the log header if the Lmsgprefix flag is provided.
6969
// The flag argument defines the logging properties.
7070
func New(out io.Writer, prefix string, flag int) *Logger {
71-
l := &Logger{out: out, prefix: prefix, flag: flag}
72-
if out == io.Discard {
73-
l.isDiscard.Store(true)
74-
}
71+
l := new(Logger)
72+
l.SetOutput(out)
73+
l.SetPrefix(prefix)
74+
l.SetFlags(flag)
7575
return l
7676
}
7777

7878
// SetOutput sets the output destination for the logger.
7979
func (l *Logger) SetOutput(w io.Writer) {
80-
l.mu.Lock()
81-
defer l.mu.Unlock()
80+
l.outMu.Lock()
81+
defer l.outMu.Unlock()
8282
l.out = w
8383
l.isDiscard.Store(w == io.Discard)
8484
}
@@ -110,15 +110,15 @@ func itoa(buf *[]byte, i int, wid int) {
110110
// - date and/or time (if corresponding flags are provided),
111111
// - file and line number (if corresponding flags are provided),
112112
// - l.prefix (if it's not blank and Lmsgprefix is set).
113-
func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
114-
if l.flag&Lmsgprefix == 0 {
115-
*buf = append(*buf, l.prefix...)
113+
func formatHeader(buf *[]byte, t time.Time, prefix string, flag int, file string, line int) {
114+
if flag&Lmsgprefix == 0 {
115+
*buf = append(*buf, prefix...)
116116
}
117-
if l.flag&(Ldate|Ltime|Lmicroseconds) != 0 {
118-
if l.flag&LUTC != 0 {
117+
if flag&(Ldate|Ltime|Lmicroseconds) != 0 {
118+
if flag&LUTC != 0 {
119119
t = t.UTC()
120120
}
121-
if l.flag&Ldate != 0 {
121+
if flag&Ldate != 0 {
122122
year, month, day := t.Date()
123123
itoa(buf, year, 4)
124124
*buf = append(*buf, '/')
@@ -127,22 +127,22 @@ func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
127127
itoa(buf, day, 2)
128128
*buf = append(*buf, ' ')
129129
}
130-
if l.flag&(Ltime|Lmicroseconds) != 0 {
130+
if flag&(Ltime|Lmicroseconds) != 0 {
131131
hour, min, sec := t.Clock()
132132
itoa(buf, hour, 2)
133133
*buf = append(*buf, ':')
134134
itoa(buf, min, 2)
135135
*buf = append(*buf, ':')
136136
itoa(buf, sec, 2)
137-
if l.flag&Lmicroseconds != 0 {
137+
if flag&Lmicroseconds != 0 {
138138
*buf = append(*buf, '.')
139139
itoa(buf, t.Nanosecond()/1e3, 6)
140140
}
141141
*buf = append(*buf, ' ')
142142
}
143143
}
144-
if l.flag&(Lshortfile|Llongfile) != 0 {
145-
if l.flag&Lshortfile != 0 {
144+
if flag&(Lshortfile|Llongfile) != 0 {
145+
if flag&Lshortfile != 0 {
146146
short := file
147147
for i := len(file) - 1; i > 0; i-- {
148148
if file[i] == '/' {
@@ -157,11 +157,32 @@ func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
157157
itoa(buf, line, -1)
158158
*buf = append(*buf, ": "...)
159159
}
160-
if l.flag&Lmsgprefix != 0 {
161-
*buf = append(*buf, l.prefix...)
160+
if flag&Lmsgprefix != 0 {
161+
*buf = append(*buf, prefix...)
162162
}
163163
}
164164

165+
var bufferPool = sync.Pool{New: func() any { return new([]byte) }}
166+
167+
func getBuffer() *[]byte {
168+
p := bufferPool.Get().(*[]byte)
169+
*p = (*p)[:0]
170+
return p
171+
}
172+
173+
func putBuffer(p *[]byte) {
174+
// Proper usage of a sync.Pool requires each entry to have approximately
175+
// the same memory cost. To obtain this property when the stored type
176+
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
177+
// to place back in the pool.
178+
//
179+
// See https://go.dev/issue/23199
180+
if cap(*p) > 64<<10 {
181+
*p = nil
182+
}
183+
bufferPool.Put(p)
184+
}
185+
165186
// Output writes the output for a logging event. The string s contains
166187
// the text to print after the prefix specified by the flags of the
167188
// Logger. A newline is appended if the last character of s is not
@@ -170,28 +191,34 @@ func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
170191
// paths it will be 2.
171192
func (l *Logger) Output(calldepth int, s string) error {
172193
now := time.Now() // get this early.
194+
195+
// Load prefix and flag once so that their value is consistent within
196+
// this call regardless of any concurrent changes to their value.
197+
prefix := l.Prefix()
198+
flag := l.Flags()
199+
173200
var file string
174201
var line int
175-
l.mu.Lock()
176-
defer l.mu.Unlock()
177-
if l.flag&(Lshortfile|Llongfile) != 0 {
178-
// Release lock while getting caller info - it's expensive.
179-
l.mu.Unlock()
202+
if flag&(Lshortfile|Llongfile) != 0 {
180203
var ok bool
181204
_, file, line, ok = runtime.Caller(calldepth)
182205
if !ok {
183206
file = "???"
184207
line = 0
185208
}
186-
l.mu.Lock()
187209
}
188-
l.buf = l.buf[:0]
189-
l.formatHeader(&l.buf, now, file, line)
190-
l.buf = append(l.buf, s...)
210+
211+
buf := getBuffer()
212+
defer putBuffer(buf)
213+
formatHeader(buf, now, prefix, flag, file, line)
214+
*buf = append(*buf, s...)
191215
if len(s) == 0 || s[len(s)-1] != '\n' {
192-
l.buf = append(l.buf, '\n')
216+
*buf = append(*buf, '\n')
193217
}
194-
_, err := l.out.Write(l.buf)
218+
219+
l.outMu.Lock()
220+
defer l.outMu.Unlock()
221+
_, err := l.out.Write(*buf)
195222
return err
196223
}
197224

@@ -264,37 +291,32 @@ func (l *Logger) Panicln(v ...any) {
264291
// Flags returns the output flags for the logger.
265292
// The flag bits are Ldate, Ltime, and so on.
266293
func (l *Logger) Flags() int {
267-
l.mu.Lock()
268-
defer l.mu.Unlock()
269-
return l.flag
294+
return int(l.flag.Load())
270295
}
271296

272297
// SetFlags sets the output flags for the logger.
273298
// The flag bits are Ldate, Ltime, and so on.
274299
func (l *Logger) SetFlags(flag int) {
275-
l.mu.Lock()
276-
defer l.mu.Unlock()
277-
l.flag = flag
300+
l.flag.Store(int32(flag))
278301
}
279302

280303
// Prefix returns the output prefix for the logger.
281304
func (l *Logger) Prefix() string {
282-
l.mu.Lock()
283-
defer l.mu.Unlock()
284-
return l.prefix
305+
if p := l.prefix.Load(); p != nil {
306+
return *p
307+
}
308+
return ""
285309
}
286310

287311
// SetPrefix sets the output prefix for the logger.
288312
func (l *Logger) SetPrefix(prefix string) {
289-
l.mu.Lock()
290-
defer l.mu.Unlock()
291-
l.prefix = prefix
313+
l.prefix.Store(&prefix)
292314
}
293315

294316
// Writer returns the output destination for the logger.
295317
func (l *Logger) Writer() io.Writer {
296-
l.mu.Lock()
297-
defer l.mu.Unlock()
318+
l.outMu.Lock()
319+
defer l.outMu.Unlock()
298320
return l.out
299321
}
300322

src/log/log_test.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import (
1212
"io"
1313
"os"
1414
"regexp"
15+
"runtime"
1516
"strings"
17+
"sync"
1618
"testing"
1719
"time"
1820
)
@@ -21,7 +23,7 @@ const (
2123
Rdate = `[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]`
2224
Rtime = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]`
2325
Rmicroseconds = `\.[0-9][0-9][0-9][0-9][0-9][0-9]`
24-
Rline = `(61|63):` // must update if the calls to l.Printf / l.Print below move
26+
Rline = `(63|65):` // must update if the calls to l.Printf / l.Print below move
2527
Rlongfile = `.*/[A-Za-z0-9_\-]+\.go:` + Rline
2628
Rshortfile = `[A-Za-z0-9_\-]+\.go:` + Rline
2729
)
@@ -98,14 +100,20 @@ func TestOutput(t *testing.T) {
98100
}
99101
}
100102

103+
func TestNonNewLogger(t *testing.T) {
104+
var l Logger
105+
l.SetOutput(new(bytes.Buffer)) // minimal work to initialize a Logger
106+
l.Print("hello")
107+
}
108+
101109
func TestOutputRace(t *testing.T) {
102110
var b bytes.Buffer
103111
l := New(&b, "", 0)
104112
for i := 0; i < 100; i++ {
105113
go func() {
106114
l.SetFlags(0)
115+
l.Output(0, "")
107116
}()
108-
l.Output(0, "")
109117
}
110118
}
111119

@@ -224,3 +232,26 @@ func BenchmarkPrintlnNoFlags(b *testing.B) {
224232
l.Println(testString)
225233
}
226234
}
235+
236+
// discard is identical to io.Discard,
237+
// but copied here to avoid the io.Discard optimization in Logger.
238+
type discard struct{}
239+
240+
func (discard) Write(p []byte) (int, error) {
241+
return len(p), nil
242+
}
243+
244+
func BenchmarkConcurrent(b *testing.B) {
245+
l := New(discard{}, "prefix: ", Ldate|Ltime|Lmicroseconds|Llongfile|Lmsgprefix)
246+
var group sync.WaitGroup
247+
for i := runtime.NumCPU(); i > 0; i-- {
248+
group.Add(1)
249+
go func() {
250+
for i := 0; i < b.N; i++ {
251+
l.Output(0, "hello, world!")
252+
}
253+
defer group.Done()
254+
}()
255+
}
256+
group.Wait()
257+
}

0 commit comments

Comments
 (0)