internal/deepprint: buffer writes

The sha256 hash writer doesn't implement WriteString.
(See https://github.com/golang/go/issues/38776.)
As a consequence, we end up converting many strings to []byte.

Wrapping a bufio.Writer around the hash writer lets us
avoid these conversions by using WriteString.

Using a bufio.Writer is, perhaps surprisingly, almost as cheap as using unsafe.
The reason is that the sha256 writer does internal buffering,
but doesn't do any when handed larger writers.
Using a bufio.Writer merely shifts the data copying from one buffer
to a different one.

Using a concrete type for Print and print cuts 10% off of the execution time.

name    old time/op    new time/op    delta
Hash-8    15.3µs ± 0%    11.5µs ± 0%  -24.84%  (p=0.000 n=10+10)

name    old alloc/op   new alloc/op   delta
Hash-8    2.82kB ± 0%    1.98kB ± 0%  -29.57%  (p=0.000 n=10+10)

name    old allocs/op  new allocs/op  delta
Hash-8       140 ± 0%        82 ± 0%  -41.43%  (p=0.000 n=10+10)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
This commit is contained in:
Josh Bleecher Snyder 2021-05-10 13:29:56 -07:00 committed by Josh Bleecher Snyder
parent 7891b34266
commit 752f8c0f2f
2 changed files with 10 additions and 11 deletions

View File

@ -12,15 +12,18 @@
package deepprint package deepprint
import ( import (
"bufio"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"io"
"reflect" "reflect"
) )
func Hash(v ...interface{}) string { func Hash(v ...interface{}) string {
h := sha256.New() h := sha256.New()
Print(h, v) // 64 matches the chunk size in crypto/sha256/sha256.go
b := bufio.NewWriterSize(h, 64)
Print(b, v)
b.Flush()
return fmt.Sprintf("%x", h.Sum(nil)) return fmt.Sprintf("%x", h.Sum(nil))
} }
@ -34,11 +37,11 @@ func UpdateHash(last *string, v ...interface{}) (changed bool) {
return false return false
} }
func Print(w io.Writer, v ...interface{}) { func Print(w *bufio.Writer, v ...interface{}) {
print(w, reflect.ValueOf(v), make(map[uintptr]bool)) print(w, reflect.ValueOf(v), make(map[uintptr]bool))
} }
func print(w io.Writer, v reflect.Value, visited map[uintptr]bool) { func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool) {
if !v.IsValid() { if !v.IsValid() {
return return
} }
@ -58,7 +61,8 @@ func print(w io.Writer, v reflect.Value, visited map[uintptr]bool) {
t := v.Type() t := v.Type()
for i, n := 0, v.NumField(); i < n; i++ { for i, n := 0, v.NumField(); i < n; i++ {
sf := t.Field(i) sf := t.Field(i)
fmt.Fprintf(w, "%s: ", sf.Name) w.WriteString(sf.Name)
w.WriteString(": ")
print(w, v.Field(i), visited) print(w, v.Field(i), visited)
fmt.Fprintf(w, "\n") fmt.Fprintf(w, "\n")
} }
@ -88,7 +92,7 @@ func print(w io.Writer, v reflect.Value, visited map[uintptr]bool) {
fmt.Fprintf(w, "}\n") fmt.Fprintf(w, "}\n")
case reflect.String: case reflect.String:
fmt.Fprintf(w, "%s", v.String()) w.WriteString(v.String())
case reflect.Bool: case reflect.Bool:
fmt.Fprintf(w, "%v", v.Bool()) fmt.Fprintf(w, "%v", v.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:

View File

@ -5,7 +5,6 @@
package deepprint package deepprint
import ( import (
"bytes"
"testing" "testing"
"inet.af/netaddr" "inet.af/netaddr"
@ -18,10 +17,6 @@ func TestDeepPrint(t *testing.T) {
// Mostly we're just testing that we don't panic on handled types. // Mostly we're just testing that we don't panic on handled types.
v := getVal() v := getVal()
var buf bytes.Buffer
Print(&buf, v)
t.Logf("Got: %s", buf.Bytes())
hash1 := Hash(v) hash1 := Hash(v)
t.Logf("hash: %v", hash1) t.Logf("hash: %v", hash1)
for i := 0; i < 20; i++ { for i := 0; i < 20; i++ {