Skip to content

Commit 608085c

Browse files
abhinavrabbbit
authored andcommitted
zap.Any: Reduce stack size with generics (#1310)
Yet another attempt at reducing the stack size of zap.Any, borrowing from #1301, #1303, #1304, #1305, #1307, and #1308. This approach defines a generic data type for field constructors of a specific type. This is similar to the lookup map in #1307, minus the map lookup, the interface match, or reflection. type anyFieldC[T any] func(string, T) Field The generic data type provides a non-generic method matching the interface: interface{ Any(string, any) Field } **Stack size**: The stack size of zap.Any following this change is 0xc0 (192 bytes). % go build -gcflags -S 2>&1 | grep ^go.uber.org/zap.Any go.uber.org/zap.Any STEXT size=5861 args=0x20 locals=0xc0 funcid=0x0 align=0x0 This is just 8 bytes more than #1305, which is the smallest stack size of all other attempts. **Allocations**: Everything appears to get inlined with no heap escapes: % go build -gcflags -m 2>&1 | grep field.go | perl -n -e 'next unless m{^./field.go:(\d+)}; print if ($1 >= 413)' | grep 'escapes' [no output] (Line 413 declares anyFieldC) Besides that, the output of `-m` for the relevant section of code consists of almost entirely: ./field.go:415:6: can inline anyFieldC[go.shape.bool].Any ./field.go:415:6: can inline anyFieldC[go.shape.[]bool].Any ./field.go:415:6: can inline anyFieldC[go.shape.complex128].Any [...] ./field.go:415:6: inlining call to anyFieldC[go.shape.complex128].Any ./field.go:415:6: inlining call to anyFieldC[go.shape.[]bool].Any ./field.go:415:6: inlining call to anyFieldC[go.shape.bool].Any Followed by: ./field.go:428:10: leaking param: key ./field.go:428:22: leaking param: value **Maintainability**: Unlike some of the other approaches, this variant is more maintainable. The `zap.Any` function looks roughly the same. Adding new branches there is obvious, and requires no duplication. **Performance**: This is a net improvement against master on BenchmarkAny's log-go checks that log inside a new goroutine. ``` name old time/op new time/op delta Any/string/field-only/typed 25.2ns ± 1% 25.6ns ± 2% ~ (p=0.460 n=5+5) Any/string/field-only/any 56.9ns ± 3% 79.4ns ± 0% +39.55% (p=0.008 n=5+5) Any/string/log/typed 1.47µs ± 0% 1.49µs ± 4% +1.58% (p=0.016 n=4+5) Any/string/log/any 1.53µs ± 2% 1.55µs ± 1% +1.37% (p=0.016 n=5+5) Any/string/log-go/typed 5.97µs ± 6% 5.99µs ± 1% ~ (p=0.151 n=5+5) Any/string/log-go/any 10.9µs ± 0% 6.2µs ± 0% -43.32% (p=0.008 n=5+5) Any/stringer/field-only/typed 25.3ns ± 1% 25.5ns ± 1% +1.09% (p=0.008 n=5+5) Any/stringer/field-only/any 85.5ns ± 1% 124.5ns ± 0% +45.66% (p=0.008 n=5+5) Any/stringer/log/typed 1.43µs ± 1% 1.42µs ± 2% ~ (p=0.175 n=4+5) Any/stringer/log/any 1.50µs ± 1% 1.56µs ± 6% +4.20% (p=0.008 n=5+5) Any/stringer/log-go/typed 5.94µs ± 0% 5.92µs ± 0% -0.40% (p=0.032 n=5+5) Any/stringer/log-go/any 11.1µs ± 2% 6.3µs ± 0% -42.93% (p=0.008 n=5+5) name old alloc/op new alloc/op delta Any/string/field-only/typed 0.00B 0.00B ~ (all equal) Any/string/field-only/any 0.00B 0.00B ~ (all equal) Any/string/log/typed 64.0B ± 0% 64.0B ± 0% ~ (all equal) Any/string/log/any 64.0B ± 0% 64.0B ± 0% ~ (all equal) Any/string/log-go/typed 112B ± 0% 112B ± 0% ~ (all equal) Any/string/log-go/any 128B ± 0% 128B ± 0% ~ (all equal) Any/stringer/field-only/typed 0.00B 0.00B ~ (all equal) Any/stringer/field-only/any 0.00B 0.00B ~ (all equal) Any/stringer/log/typed 64.0B ± 0% 64.0B ± 0% ~ (all equal) Any/stringer/log/any 64.0B ± 0% 64.0B ± 0% ~ (all equal) Any/stringer/log-go/typed 112B ± 0% 112B ± 0% ~ (all equal) Any/stringer/log-go/any 128B ± 0% 128B ± 0% ~ (all equal) name old allocs/op new allocs/op delta Any/string/field-only/typed 0.00 0.00 ~ (all equal) Any/string/field-only/any 0.00 0.00 ~ (all equal) Any/string/log/typed 1.00 ± 0% 1.00 ± 0% ~ (all equal) Any/string/log/any 1.00 ± 0% 1.00 ± 0% ~ (all equal) Any/string/log-go/typed 3.00 ± 0% 3.00 ± 0% ~ (all equal) Any/string/log-go/any 3.00 ± 0% 3.00 ± 0% ~ (all equal) Any/stringer/field-only/typed 0.00 0.00 ~ (all equal) Any/stringer/field-only/any 0.00 0.00 ~ (all equal) Any/stringer/log/typed 1.00 ± 0% 1.00 ± 0% ~ (all equal) Any/stringer/log/any 1.00 ± 0% 1.00 ± 0% ~ (all equal) Any/stringer/log-go/typed 3.00 ± 0% 3.00 ± 0% ~ (all equal) Any/stringer/log-go/any 3.00 ± 0% 3.00 ± 0% ~ (all equal) ``` It causes a regression in "field-only" which calls the field constructor and discards the result without using it in a logger. I believe this is acceptable because that's not a real use case; we expect the result to be used with a logger.
1 parent cd2f6ac commit 608085c

File tree

1 file changed

+105
-64
lines changed

1 file changed

+105
-64
lines changed

field.go

Lines changed: 105 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,43 @@ func Inline(val zapcore.ObjectMarshaler) Field {
410410
}
411411
}
412412

413+
// We discovered an issue where zap.Any can cause a performance degradation
414+
// when used in new goroutines.
415+
//
416+
// This happens because the compiler assigns 4.8kb (one zap.Field per arm of
417+
// switch statement) of stack space for zap.Any when it takes the form:
418+
//
419+
// switch v := v.(type) {
420+
// case string:
421+
// return String(key, v)
422+
// case int:
423+
// return Int(key, v)
424+
// // ...
425+
// default:
426+
// return Reflect(key, v)
427+
// }
428+
//
429+
// To avoid this, we use the type switch to assign a value to a single local variable
430+
// and then call a function on it.
431+
// The local variable is just a function reference so it doesn't allocate
432+
// when converted to an interface{}.
433+
//
434+
// A fair bit of experimentation went into this.
435+
// See also:
436+
//
437+
// - https://github.com/uber-go/zap/pull/1301
438+
// - https://github.com/uber-go/zap/pull/1303
439+
// - https://github.com/uber-go/zap/pull/1304
440+
// - https://github.com/uber-go/zap/pull/1305
441+
// - https://github.com/uber-go/zap/pull/1308
442+
type anyFieldC[T any] func(string, T) Field
443+
444+
func (f anyFieldC[T]) Any(key string, val any) Field {
445+
v, _ := val.(T)
446+
// val is guaranteed to be a T, except when it's nil.
447+
return f(key, v)
448+
}
449+
413450
// Any takes a key and an arbitrary value and chooses the best way to represent
414451
// them as a field, falling back to a reflection-based approach only if
415452
// necessary.
@@ -418,132 +455,136 @@ func Inline(val zapcore.ObjectMarshaler) Field {
418455
// them. To minimize surprises, []byte values are treated as binary blobs, byte
419456
// values are treated as uint8, and runes are always treated as integers.
420457
func Any(key string, value interface{}) Field {
421-
switch val := value.(type) {
458+
var c interface{ Any(string, any) Field }
459+
460+
switch value.(type) {
422461
case zapcore.ObjectMarshaler:
423-
return Object(key, val)
462+
c = anyFieldC[zapcore.ObjectMarshaler](Object)
424463
case zapcore.ArrayMarshaler:
425-
return Array(key, val)
464+
c = anyFieldC[zapcore.ArrayMarshaler](Array)
426465
case bool:
427-
return Bool(key, val)
466+
c = anyFieldC[bool](Bool)
428467
case *bool:
429-
return Boolp(key, val)
468+
c = anyFieldC[*bool](Boolp)
430469
case []bool:
431-
return Bools(key, val)
470+
c = anyFieldC[[]bool](Bools)
432471
case complex128:
433-
return Complex128(key, val)
472+
c = anyFieldC[complex128](Complex128)
434473
case *complex128:
435-
return Complex128p(key, val)
474+
c = anyFieldC[*complex128](Complex128p)
436475
case []complex128:
437-
return Complex128s(key, val)
476+
c = anyFieldC[[]complex128](Complex128s)
438477
case complex64:
439-
return Complex64(key, val)
478+
c = anyFieldC[complex64](Complex64)
440479
case *complex64:
441-
return Complex64p(key, val)
480+
c = anyFieldC[*complex64](Complex64p)
442481
case []complex64:
443-
return Complex64s(key, val)
482+
c = anyFieldC[[]complex64](Complex64s)
444483
case float64:
445-
return Float64(key, val)
484+
c = anyFieldC[float64](Float64)
446485
case *float64:
447-
return Float64p(key, val)
486+
c = anyFieldC[*float64](Float64p)
448487
case []float64:
449-
return Float64s(key, val)
488+
c = anyFieldC[[]float64](Float64s)
450489
case float32:
451-
return Float32(key, val)
490+
c = anyFieldC[float32](Float32)
452491
case *float32:
453-
return Float32p(key, val)
492+
c = anyFieldC[*float32](Float32p)
454493
case []float32:
455-
return Float32s(key, val)
494+
c = anyFieldC[[]float32](Float32s)
456495
case int:
457-
return Int(key, val)
496+
c = anyFieldC[int](Int)
458497
case *int:
459-
return Intp(key, val)
498+
c = anyFieldC[*int](Intp)
460499
case []int:
461-
return Ints(key, val)
500+
c = anyFieldC[[]int](Ints)
462501
case int64:
463-
return Int64(key, val)
502+
c = anyFieldC[int64](Int64)
464503
case *int64:
465-
return Int64p(key, val)
504+
c = anyFieldC[*int64](Int64p)
466505
case []int64:
467-
return Int64s(key, val)
506+
c = anyFieldC[[]int64](Int64s)
468507
case int32:
469-
return Int32(key, val)
508+
c = anyFieldC[int32](Int32)
470509
case *int32:
471-
return Int32p(key, val)
510+
c = anyFieldC[*int32](Int32p)
472511
case []int32:
473-
return Int32s(key, val)
512+
c = anyFieldC[[]int32](Int32s)
474513
case int16:
475-
return Int16(key, val)
514+
c = anyFieldC[int16](Int16)
476515
case *int16:
477-
return Int16p(key, val)
516+
c = anyFieldC[*int16](Int16p)
478517
case []int16:
479-
return Int16s(key, val)
518+
c = anyFieldC[[]int16](Int16s)
480519
case int8:
481-
return Int8(key, val)
520+
c = anyFieldC[int8](Int8)
482521
case *int8:
483-
return Int8p(key, val)
522+
c = anyFieldC[*int8](Int8p)
484523
case []int8:
485-
return Int8s(key, val)
524+
c = anyFieldC[[]int8](Int8s)
486525
case string:
487-
return String(key, val)
526+
c = anyFieldC[string](String)
488527
case *string:
489-
return Stringp(key, val)
528+
c = anyFieldC[*string](Stringp)
490529
case []string:
491-
return Strings(key, val)
530+
c = anyFieldC[[]string](Strings)
492531
case uint:
493-
return Uint(key, val)
532+
c = anyFieldC[uint](Uint)
494533
case *uint:
495-
return Uintp(key, val)
534+
c = anyFieldC[*uint](Uintp)
496535
case []uint:
497-
return Uints(key, val)
536+
c = anyFieldC[[]uint](Uints)
498537
case uint64:
499-
return Uint64(key, val)
538+
c = anyFieldC[uint64](Uint64)
500539
case *uint64:
501-
return Uint64p(key, val)
540+
c = anyFieldC[*uint64](Uint64p)
502541
case []uint64:
503-
return Uint64s(key, val)
542+
c = anyFieldC[[]uint64](Uint64s)
504543
case uint32:
505-
return Uint32(key, val)
544+
c = anyFieldC[uint32](Uint32)
506545
case *uint32:
507-
return Uint32p(key, val)
546+
c = anyFieldC[*uint32](Uint32p)
508547
case []uint32:
509-
return Uint32s(key, val)
548+
c = anyFieldC[[]uint32](Uint32s)
510549
case uint16:
511-
return Uint16(key, val)
550+
c = anyFieldC[uint16](Uint16)
512551
case *uint16:
513-
return Uint16p(key, val)
552+
c = anyFieldC[*uint16](Uint16p)
514553
case []uint16:
515-
return Uint16s(key, val)
554+
c = anyFieldC[[]uint16](Uint16s)
516555
case uint8:
517-
return Uint8(key, val)
556+
c = anyFieldC[uint8](Uint8)
518557
case *uint8:
519-
return Uint8p(key, val)
558+
c = anyFieldC[*uint8](Uint8p)
520559
case []byte:
521-
return Binary(key, val)
560+
c = anyFieldC[[]byte](Binary)
522561
case uintptr:
523-
return Uintptr(key, val)
562+
c = anyFieldC[uintptr](Uintptr)
524563
case *uintptr:
525-
return Uintptrp(key, val)
564+
c = anyFieldC[*uintptr](Uintptrp)
526565
case []uintptr:
527-
return Uintptrs(key, val)
566+
c = anyFieldC[[]uintptr](Uintptrs)
528567
case time.Time:
529-
return Time(key, val)
568+
c = anyFieldC[time.Time](Time)
530569
case *time.Time:
531-
return Timep(key, val)
570+
c = anyFieldC[*time.Time](Timep)
532571
case []time.Time:
533-
return Times(key, val)
572+
c = anyFieldC[[]time.Time](Times)
534573
case time.Duration:
535-
return Duration(key, val)
574+
c = anyFieldC[time.Duration](Duration)
536575
case *time.Duration:
537-
return Durationp(key, val)
576+
c = anyFieldC[*time.Duration](Durationp)
538577
case []time.Duration:
539-
return Durations(key, val)
578+
c = anyFieldC[[]time.Duration](Durations)
540579
case error:
541-
return NamedError(key, val)
580+
c = anyFieldC[error](NamedError)
542581
case []error:
543-
return Errors(key, val)
582+
c = anyFieldC[[]error](Errors)
544583
case fmt.Stringer:
545-
return Stringer(key, val)
584+
c = anyFieldC[fmt.Stringer](Stringer)
546585
default:
547-
return Reflect(key, val)
586+
c = anyFieldC[any](Reflect)
548587
}
588+
589+
return c.Any(key, value)
549590
}

0 commit comments

Comments
 (0)