@@ -894,6 +894,122 @@ error.
894
894
895
895
## Speed
896
896
897
- TODO(jba): discuss
897
+ Most programs don't need fast logging.
898
+ Before making your handler fast, gather data&mdash ; preferably production data,
899
+ not benchmark comparisons&mdash ; that demonstrates that it needs to be fast.
900
+ Avoid premature optimization.
898
901
899
- TODO(jba): show how to pool a [ ] byte.
902
+ If you need a fast handler, start with pre-formatting. It may provide dramatic
903
+ speed-ups in cases where a single call to ` Logger.With ` is followed by many
904
+ calls to the resulting logger.
905
+
906
+ If log output is the bottleneck, consider making your handler asynchronous.
907
+ Do the minimal amount of processing in the handler, then send the record and
908
+ other information over a channel. Another goroutine can collect the incoming log
909
+ entries and write them in bulk and in the background.
910
+ You might want to preserve the option to log synchronously
911
+ so you can see all the log output to debug a crash.
912
+
913
+ Allocation is often a major cause of a slow system.
914
+ The ` slog ` package already works hard at minimizing allocations.
915
+ If your handler does it own allocation, and profiling shows it to be
916
+ a problem, then see if you can minimize it.
917
+
918
+ One simple change you can make is to replace calls to ` fmt.Sprintf ` or ` fmt.Appendf `
919
+ with direct appends to the buffer. For example, our IndentHandler appends string
920
+ attributes to the buffer like so:
921
+
922
+ buf = fmt.Appendf(buf, "%s: %q\n", a.Key, a.Value.String())
923
+
924
+ As of Go 1.21, that results in two allocations, one for each argument passed to
925
+ an ` any ` parameter. We can get that down to zero by using ` append ` directly:
926
+
927
+ buf = append(buf, a.Key...)
928
+ buf = append(buf, ": "...)
929
+ buf = strconv.AppendQuote(buf, a.Value.String())
930
+ buf = append(buf, '\n')
931
+
932
+ Another worthwhile change is to use a ` sync.Pool ` to manage the one chunk of
933
+ memory that most handlers need:
934
+ the ` []byte ` buffer holding the formatted output.
935
+
936
+ Our example ` Handle ` method began with this line:
937
+
938
+ buf := make([]byte, 0, 1024)
939
+
940
+ As we said above, providing a large initial capacity avoids repeated copying and
941
+ re-allocation of the slice as it grows, reducing the number of allocations to
942
+ one.
943
+ But we can get it down to zero in the steady state by keeping a global pool of buffers.
944
+ Initially, the pool will be empty and new buffers will be allocated.
945
+ But eventually, assuming the number of concurrent log calls reaches a steady
946
+ maximum, there will be enough buffers in the pool to share among all the
947
+ ongoing ` Handler ` calls. As long as no log entry grows past a buffer's capacity,
948
+ there will be no allocations from the garbage collector's point of view.
949
+
950
+ We will hide our pool behind a pair of functions, ` allocBuf ` and ` freeBuf ` .
951
+ The single line to get a buffer at the top of ` Handle ` becomes two lines:
952
+
953
+ bufp := allocBuf()
954
+ defer freeBuf(bufp)
955
+
956
+ One of the subtleties involved in making a ` sync.Pool ` of slices
957
+ is suggested by the variable name ` bufp ` : your pool must deal in
958
+ _ pointers_ to slices, not the slices themselves.
959
+ Pooled values must always be pointers. If they aren't, then the ` any ` arguments
960
+ and return values of the ` sync.Pool ` methods will themselves cause allocations,
961
+ defeating the purpose of pooling.
962
+
963
+ There are two ways to proceed with our slice pointer: we can replace ` buf `
964
+ with ` *bufp ` throughout our function, or we can dereference it and remember to
965
+ re-assign it before freeing:
966
+
967
+ bufp := allocBuf()
968
+ buf := *bufp
969
+ defer func() {
970
+ *bufp = buf
971
+ freeBuf(bufp)
972
+ }()
973
+
974
+
975
+ Here is our pool and its associated functions:
976
+
977
+ ```
978
+ var bufPool = sync.Pool{
979
+ New: func() any {
980
+ b := make([]byte, 0, 1024)
981
+ return &b
982
+ },
983
+ }
984
+
985
+ func allocBuf() *[]byte {
986
+ return bufPool.Get().(*[]byte)
987
+ }
988
+
989
+ func freeBuf(b *[]byte) {
990
+ // To reduce peak allocation, return only smaller buffers to the pool.
991
+ const maxBufferSize = 16 << 10
992
+ if cap(*b) <= maxBufferSize {
993
+ *b = (*b)[:0]
994
+ bufPool.Put(b)
995
+ }
996
+ }
997
+ ```
998
+
999
+ The pool's ` New ` function does the same thing as the original code:
1000
+ create a byte slice with 0 length and plenty of capacity.
1001
+ The ` allocBuf ` function just type-asserts the result of the pool's
1002
+ ` Get ` method.
1003
+
1004
+ The ` freeBuf ` method truncates the buffer before putting it back
1005
+ in the pool, so that ` allocBuf ` always returns zero-length slices.
1006
+ It also implements an important optimization: it doesn't return
1007
+ large buffers to the pool.
1008
+ To see why this important, consider what would happen if there were a single,
1009
+ unusually large log entry&mdash ; say one that was a megabyte when formatted.
1010
+ If that megabyte-sized buffer were put in the pool, it could remain
1011
+ there indefinitely, constantly being reused, but with most of its capacity
1012
+ wasted.
1013
+ The extra memory might never be used again by the handler, and since it was in
1014
+ the handler's pool, it might never be garbage-collected for reuse elsewhere.
1015
+ We can avoid that situation by keeping large buffers out of the pool.
0 commit comments