diff --git a/src/crypto/internal/fips140/sha512/sha512.go b/src/crypto/internal/fips140/sha512/sha512.go index 3e7a5e11f15198..79bd61a4e39ab6 100644 --- a/src/crypto/internal/fips140/sha512/sha512.go +++ b/src/crypto/internal/fips140/sha512/sha512.go @@ -31,6 +31,11 @@ const ( blockSize = 128 ) +// The maximum number of bytes that can be passed to block(). The limit exists +// because implementations that rely on assembly routines are not preemptible. +const maxAsmIters = 512 +const maxAsmSize = blockSize * maxAsmIters // 64KiB + const ( chunk = 128 init0 = 0x6a09e667f3bcc908 @@ -248,6 +253,11 @@ func (d *Digest) Write(p []byte) (nn int, err error) { } if len(p) >= chunk { n := len(p) &^ (chunk - 1) + for n > maxAsmSize { + block(d, p[:maxAsmSize]) + p = p[maxAsmSize:] + n -= maxAsmSize + } block(d, p[:n]) p = p[n:] } diff --git a/src/crypto/sha1/sha1.go b/src/crypto/sha1/sha1.go index 46e47df1d32cf2..4e117201485bf3 100644 --- a/src/crypto/sha1/sha1.go +++ b/src/crypto/sha1/sha1.go @@ -27,6 +27,11 @@ const Size = 20 // The blocksize of SHA-1 in bytes. const BlockSize = 64 +// The maximum number of bytes that can be passed to block(). The limit exists +// because implementations that rely on assembly routines are not preemptible. +const maxAsmIters = 1024 +const maxAsmSize = BlockSize * maxAsmIters // 64KiB + const ( chunk = 64 init0 = 0x67452301 @@ -143,6 +148,11 @@ func (d *digest) Write(p []byte) (nn int, err error) { } if len(p) >= chunk { n := len(p) &^ (chunk - 1) + for n > maxAsmSize { + block(d, p[:maxAsmSize]) + p = p[maxAsmSize:] + n -= maxAsmSize + } block(d, p[:n]) p = p[n:] } diff --git a/src/crypto/sha1/sha1_test.go b/src/crypto/sha1/sha1_test.go index 8b4618df556271..7ab7fba078e9de 100644 --- a/src/crypto/sha1/sha1_test.go +++ b/src/crypto/sha1/sha1_test.go @@ -14,7 +14,9 @@ import ( "fmt" "hash" "io" + "runtime" "testing" + "time" ) type sha1Test struct { @@ -267,16 +269,16 @@ func TestOutOfBoundsRead(t *testing.T) { } var bench = New() -var buf = make([]byte, 8192) func benchmarkSize(b *testing.B, size int) { + buf := make([]byte, size) sum := make([]byte, bench.Size()) b.Run("New", func(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(size)) for i := 0; i < b.N; i++ { bench.Reset() - bench.Write(buf[:size]) + bench.Write(buf) bench.Sum(sum[:0]) } }) @@ -284,7 +286,7 @@ func benchmarkSize(b *testing.B, size int) { b.ReportAllocs() b.SetBytes(int64(size)) for i := 0; i < b.N; i++ { - Sum(buf[:size]) + Sum(buf) } }) } @@ -304,3 +306,40 @@ func BenchmarkHash1K(b *testing.B) { func BenchmarkHash8K(b *testing.B) { benchmarkSize(b, 8192) } + +func BenchmarkHash256K(b *testing.B) { + benchmarkSize(b, 256*1024) +} + +func BenchmarkHash1M(b *testing.B) { + benchmarkSize(b, 1024*1024) +} + +var sinkSTW []byte + +// BenchmarkSTW reports how long a garbage collection had to wait while a hash +// ran alongside it, as gcwait-ns/op. Assembly is not preemptible, so a call +// that covers the whole input blocks every goroutine in the process for as +// long as it runs; bounding the call gives the collector a way in between +// chunks. Run with GOMAXPROCS>=2 so the two actually overlap. +func BenchmarkSTW(b *testing.B) { + buf := make([]byte, 64<<20) + var total time.Duration + var iters int + b.SetBytes(int64(len(buf))) + for b.Loop() { + done := make(chan struct{}) + go func() { + defer close(done) + h := New() + h.Write(buf) + sinkSTW = h.Sum(nil) + }() + start := time.Now() + runtime.GC() // one per iteration, so the mean is well defined + total += time.Since(start) + iters++ + <-done + } + b.ReportMetric(float64(total.Nanoseconds())/float64(iters), "gcwait-ns/op") +} diff --git a/src/crypto/sha512/sha512_test.go b/src/crypto/sha512/sha512_test.go index 080bf694f03652..a222a5f3ce4d43 100644 --- a/src/crypto/sha512/sha512_test.go +++ b/src/crypto/sha512/sha512_test.go @@ -14,7 +14,9 @@ import ( "fmt" "hash" "io" + "runtime" "testing" + "time" ) type sha512Test struct { @@ -998,16 +1000,16 @@ func maybeCloner(h hash.Hash) any { } var bench = New() -var buf = make([]byte, 8192) func benchmarkSize(b *testing.B, size int) { + buf := make([]byte, size) sum := make([]byte, bench.Size()) b.Run("New", func(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(size)) for i := 0; i < b.N; i++ { bench.Reset() - bench.Write(buf[:size]) + bench.Write(buf) bench.Sum(sum[:0]) } }) @@ -1015,14 +1017,14 @@ func benchmarkSize(b *testing.B, size int) { b.ReportAllocs() b.SetBytes(int64(size)) for i := 0; i < b.N; i++ { - Sum384(buf[:size]) + Sum384(buf) } }) b.Run("Sum512", func(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(size)) for i := 0; i < b.N; i++ { - Sum512(buf[:size]) + Sum512(buf) } }) } @@ -1038,3 +1040,40 @@ func BenchmarkHash1K(b *testing.B) { func BenchmarkHash8K(b *testing.B) { benchmarkSize(b, 8192) } + +func BenchmarkHash256K(b *testing.B) { + benchmarkSize(b, 256*1024) +} + +func BenchmarkHash1M(b *testing.B) { + benchmarkSize(b, 1024*1024) +} + +var sinkSTW []byte + +// BenchmarkSTW reports how long a garbage collection had to wait while a hash +// ran alongside it, as gcwait-ns/op. Assembly is not preemptible, so a call +// that covers the whole input blocks every goroutine in the process for as +// long as it runs; bounding the call gives the collector a way in between +// chunks. Run with GOMAXPROCS>=2 so the two actually overlap. +func BenchmarkSTW(b *testing.B) { + buf := make([]byte, 64<<20) + var total time.Duration + var iters int + b.SetBytes(int64(len(buf))) + for b.Loop() { + done := make(chan struct{}) + go func() { + defer close(done) + h := New() + h.Write(buf) + sinkSTW = h.Sum(nil) + }() + start := time.Now() + runtime.GC() // one per iteration, so the mean is well defined + total += time.Since(start) + iters++ + <-done + } + b.ReportMetric(float64(total.Nanoseconds())/float64(iters), "gcwait-ns/op") +}