Skip to content

Commit 047bda0

Browse files
committed
Optimize zap.Any to allocate less mamory on stack
We have identified an issue where zap.Any can cause performance degradation due to stack size. This is apparently cased by the compiler assigning 4.8kb (a zap.Field per arm of the switch statement) for zap.Any on stack. This can result in an unnecessary runtime.newstack+runtime.copystack. A github issue against Go language is pending. This can be particularly bad if `zap.Any` was to be used in a new goroutine, since the default goroutine sizes can be as low as 2kb (it can vary depending on average stack size - see golang/go#18138). This is an alternative to #1301, @cdvr and me were talking about this, and he inspired this idea with the closure. By using a function and a closure we're able to reduce the size and remove the degradation. At least on my laptop, this change result in a new performance gain, as all benchmarks show reduced time. 10 runs. ``` ❯ benchstat ~/before.txt ~/after-after.txt goos: darwin goarch: arm64 pkg: go.uber.org/zap │ /Users/pawel/before.txt │ /Users/pawel/after-after.txt │ │ sec/op │ sec/op vs base │ AnyInGoroutine/any-12 305.2n ± 3% 297.0n ± 0% ~ (p=0.085 n=10) AnyInGoroutine/int-12 288.0n ± 0% 270.1n ± 1% -6.25% (p=0.000 n=10) AnyInGoroutine/goroutine-12 218.3n ± 6% 209.5n ± 5% -4.05% (p=0.015 n=10) AnyInGoroutine/int-in-go-12 592.7n ± 2% 573.9n ± 2% -3.17% (p=0.000 n=10) AnyInGoroutine/any-in-go-12 666.5n ± 4% 552.3n ± 1% -17.13% (p=0.000 n=10) AnyInGoroutine/int-in-go-with-stack-12 474.4n ± 4% 459.4n ± 6% ~ (p=0.447 n=10+9) AnyInGoroutine/any-in-go-with-stack-12 617.6n ± 4% 468.8n ± 4% -24.09% (p=0.000 n=10) geomean 417.8n 380.1n -9.01% │ /Users/pawel/before.txt │ /Users/pawel/after-after.txt │ │ B/op │ B/op vs base │ AnyInGoroutine/any-12 64.00 ± 0% 64.00 ± 0% ~ (p=1.000 n=10) ¹ AnyInGoroutine/int-12 64.00 ± 0% 64.00 ± 0% ~ (p=1.000 n=10) ¹ AnyInGoroutine/goroutine-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ AnyInGoroutine/int-in-go-12 88.00 ± 0% 88.00 ± 0% ~ (p=1.000 n=10) AnyInGoroutine/any-in-go-12 88.00 ± 0% 88.00 ± 0% ~ (p=1.000 n=10) AnyInGoroutine/int-in-go-with-stack-12 88.00 ± 0% 88.00 ± 0% ~ (p=1.000 n=10+9) ¹ AnyInGoroutine/any-in-go-with-stack-12 88.00 ± 0% 88.00 ± 0% ~ (p=1.000 n=10) ¹ geomean ² +0.00% ² ¹ all samples are equal ² summaries must be >0 to compute geomean │ /Users/pawel/before.txt │ /Users/pawel/after-after.txt │ │ allocs/op │ allocs/op vs base │ AnyInGoroutine/any-12 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=10) ¹ AnyInGoroutine/int-12 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=10) ¹ AnyInGoroutine/goroutine-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ AnyInGoroutine/int-in-go-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10) ¹ AnyInGoroutine/any-in-go-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10) ¹ AnyInGoroutine/int-in-go-with-stack-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10+9) ¹ AnyInGoroutine/any-in-go-with-stack-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10) ¹ geomean ² +0.00% ² ¹ all samples are equal ² summaries must be >0 to compute geomean ```
1 parent 382e251 commit 047bda0

File tree

2 files changed

+301
-63
lines changed

2 files changed

+301
-63
lines changed

field.go

Lines changed: 194 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -418,132 +418,263 @@ func Inline(val zapcore.ObjectMarshaler) Field {
418418
// them. To minimize surprises, []byte values are treated as binary blobs, byte
419419
// values are treated as uint8, and runes are always treated as integers.
420420
func Any(key string, value interface{}) Field {
421+
// To work around go compiler assigning unreasonably large space on stack
422+
// (4kb, one `Field` per arm of the switch statement) which can trigger
423+
// performance degradation if `Any` is used in a brand new goroutine.
424+
var f func() Field
421425
switch val := value.(type) {
422426
case zapcore.ObjectMarshaler:
423-
return Object(key, val)
427+
f = func() Field {
428+
return Object(key, val)
429+
}
424430
case zapcore.ArrayMarshaler:
425-
return Array(key, val)
431+
f = func() Field {
432+
return Array(key, val)
433+
}
426434
case bool:
427-
return Bool(key, val)
435+
f = func() Field {
436+
return Bool(key, val)
437+
}
428438
case *bool:
429-
return Boolp(key, val)
439+
f = func() Field {
440+
return Boolp(key, val)
441+
}
430442
case []bool:
431-
return Bools(key, val)
443+
f = func() Field {
444+
return Bools(key, val)
445+
}
432446
case complex128:
433-
return Complex128(key, val)
447+
f = func() Field {
448+
return Complex128(key, val)
449+
}
434450
case *complex128:
435-
return Complex128p(key, val)
451+
f = func() Field {
452+
return Complex128p(key, val)
453+
}
436454
case []complex128:
437-
return Complex128s(key, val)
455+
f = func() Field {
456+
return Complex128s(key, val)
457+
}
438458
case complex64:
439-
return Complex64(key, val)
459+
f = func() Field {
460+
return Complex64(key, val)
461+
}
440462
case *complex64:
441-
return Complex64p(key, val)
463+
f = func() Field {
464+
return Complex64p(key, val)
465+
}
442466
case []complex64:
443-
return Complex64s(key, val)
467+
f = func() Field {
468+
return Complex64s(key, val)
469+
}
444470
case float64:
445-
return Float64(key, val)
471+
f = func() Field {
472+
return Float64(key, val)
473+
}
446474
case *float64:
447-
return Float64p(key, val)
475+
f = func() Field {
476+
return Float64p(key, val)
477+
}
448478
case []float64:
449-
return Float64s(key, val)
479+
f = func() Field {
480+
return Float64s(key, val)
481+
}
450482
case float32:
451-
return Float32(key, val)
483+
f = func() Field {
484+
return Float32(key, val)
485+
}
452486
case *float32:
453-
return Float32p(key, val)
487+
f = func() Field {
488+
return Float32p(key, val)
489+
}
454490
case []float32:
455-
return Float32s(key, val)
491+
f = func() Field {
492+
return Float32s(key, val)
493+
}
456494
case int:
457-
return Int(key, val)
495+
f = func() Field {
496+
return Int(key, val)
497+
}
458498
case *int:
459-
return Intp(key, val)
499+
f = func() Field {
500+
return Intp(key, val)
501+
}
460502
case []int:
461-
return Ints(key, val)
503+
f = func() Field {
504+
return Ints(key, val)
505+
}
462506
case int64:
463-
return Int64(key, val)
507+
f = func() Field {
508+
return Int64(key, val)
509+
}
464510
case *int64:
465-
return Int64p(key, val)
511+
f = func() Field {
512+
return Int64p(key, val)
513+
}
466514
case []int64:
467-
return Int64s(key, val)
515+
f = func() Field {
516+
return Int64s(key, val)
517+
}
468518
case int32:
469-
return Int32(key, val)
519+
f = func() Field {
520+
return Int32(key, val)
521+
}
470522
case *int32:
471-
return Int32p(key, val)
523+
f = func() Field {
524+
return Int32p(key, val)
525+
}
472526
case []int32:
473-
return Int32s(key, val)
527+
f = func() Field {
528+
return Int32s(key, val)
529+
}
474530
case int16:
475-
return Int16(key, val)
531+
f = func() Field {
532+
return Int16(key, val)
533+
}
476534
case *int16:
477-
return Int16p(key, val)
535+
f = func() Field {
536+
return Int16p(key, val)
537+
}
478538
case []int16:
479-
return Int16s(key, val)
539+
f = func() Field {
540+
return Int16s(key, val)
541+
}
480542
case int8:
481-
return Int8(key, val)
543+
f = func() Field {
544+
return Int8(key, val)
545+
}
482546
case *int8:
483-
return Int8p(key, val)
547+
f = func() Field {
548+
return Int8p(key, val)
549+
}
484550
case []int8:
485-
return Int8s(key, val)
551+
f = func() Field {
552+
return Int8s(key, val)
553+
}
486554
case string:
487-
return String(key, val)
555+
f = func() Field {
556+
return String(key, val)
557+
}
488558
case *string:
489-
return Stringp(key, val)
559+
f = func() Field {
560+
return Stringp(key, val)
561+
}
490562
case []string:
491-
return Strings(key, val)
563+
f = func() Field {
564+
return Strings(key, val)
565+
}
492566
case uint:
493-
return Uint(key, val)
567+
f = func() Field {
568+
return Uint(key, val)
569+
}
494570
case *uint:
495-
return Uintp(key, val)
571+
f = func() Field {
572+
return Uintp(key, val)
573+
}
496574
case []uint:
497-
return Uints(key, val)
575+
f = func() Field {
576+
return Uints(key, val)
577+
}
498578
case uint64:
499-
return Uint64(key, val)
579+
f = func() Field {
580+
return Uint64(key, val)
581+
}
500582
case *uint64:
501-
return Uint64p(key, val)
583+
f = func() Field {
584+
return Uint64p(key, val)
585+
}
502586
case []uint64:
503-
return Uint64s(key, val)
587+
f = func() Field {
588+
return Uint64s(key, val)
589+
}
504590
case uint32:
505-
return Uint32(key, val)
591+
f = func() Field {
592+
return Uint32(key, val)
593+
}
506594
case *uint32:
507-
return Uint32p(key, val)
595+
f = func() Field {
596+
return Uint32p(key, val)
597+
}
508598
case []uint32:
509-
return Uint32s(key, val)
599+
f = func() Field {
600+
return Uint32s(key, val)
601+
}
510602
case uint16:
511-
return Uint16(key, val)
603+
f = func() Field {
604+
return Uint16(key, val)
605+
}
512606
case *uint16:
513-
return Uint16p(key, val)
607+
f = func() Field {
608+
return Uint16p(key, val)
609+
}
514610
case []uint16:
515-
return Uint16s(key, val)
611+
f = func() Field {
612+
return Uint16s(key, val)
613+
}
516614
case uint8:
517-
return Uint8(key, val)
615+
f = func() Field {
616+
return Uint8(key, val)
617+
}
518618
case *uint8:
519-
return Uint8p(key, val)
619+
f = func() Field {
620+
return Uint8p(key, val)
621+
}
520622
case []byte:
521-
return Binary(key, val)
623+
f = func() Field {
624+
return Binary(key, val)
625+
}
522626
case uintptr:
523-
return Uintptr(key, val)
627+
f = func() Field {
628+
return Uintptr(key, val)
629+
}
524630
case *uintptr:
525-
return Uintptrp(key, val)
631+
f = func() Field {
632+
return Uintptrp(key, val)
633+
}
526634
case []uintptr:
527-
return Uintptrs(key, val)
635+
f = func() Field {
636+
return Uintptrs(key, val)
637+
}
528638
case time.Time:
529-
return Time(key, val)
639+
f = func() Field {
640+
return Time(key, val)
641+
}
530642
case *time.Time:
531-
return Timep(key, val)
643+
f = func() Field {
644+
return Timep(key, val)
645+
}
532646
case []time.Time:
533-
return Times(key, val)
647+
f = func() Field {
648+
return Times(key, val)
649+
}
534650
case time.Duration:
535-
return Duration(key, val)
651+
f = func() Field {
652+
return Duration(key, val)
653+
}
536654
case *time.Duration:
537-
return Durationp(key, val)
655+
f = func() Field {
656+
return Durationp(key, val)
657+
}
538658
case []time.Duration:
539-
return Durations(key, val)
659+
f = func() Field {
660+
return Durations(key, val)
661+
}
540662
case error:
541-
return NamedError(key, val)
663+
f = func() Field {
664+
return NamedError(key, val)
665+
}
542666
case []error:
543-
return Errors(key, val)
667+
f = func() Field {
668+
return Errors(key, val)
669+
}
544670
case fmt.Stringer:
545-
return Stringer(key, val)
671+
f = func() Field {
672+
return Stringer(key, val)
673+
}
546674
default:
547-
return Reflect(key, val)
675+
f = func() Field {
676+
return Reflect(key, val)
677+
}
548678
}
679+
return f()
549680
}

0 commit comments

Comments
 (0)