Skip to content

Commit 94d8ac7

Browse files
committed
perf: bmt simd hasher
1 parent 2f1dade commit 94d8ac7

27 files changed

Lines changed: 1108 additions & 321 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ require (
100100
github.com/ipfs/go-log/v2 v2.6.0 // indirect
101101
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
102102
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
103-
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
103+
github.com/klauspost/cpuid/v2 v2.3.0
104104
github.com/koron/go-ssdp v0.0.6 // indirect
105105
github.com/leodido/go-urn v1.4.0 // indirect
106106
github.com/libdns/libdns v0.2.2 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,8 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
527527
github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
528528
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
529529
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
530-
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
531-
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
530+
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
531+
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
532532
github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
533533
github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
534534
github.com/klauspost/reedsolomon v1.11.8 h1:s8RpUW5TK4hjr+djiOpbZJB4ksx+TdYbRH7vHQpwPOY=

pkg/bmt/benchmark_test.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ func BenchmarkBMT(b *testing.B) {
2929
b.Run(fmt.Sprintf("%v_size_%v", "BMT", size), func(b *testing.B) {
3030
benchmarkBMT(b, size)
3131
})
32+
b.Run(fmt.Sprintf("%v_size_%v", "BMT_NoSIMD", size), func(b *testing.B) {
33+
benchmarkBMTNoSIMD(b, size)
34+
})
3235
}
3336
}
3437

@@ -87,7 +90,7 @@ func benchmarkBMT(b *testing.B, n int) {
8790

8891
testData := testutil.RandBytesWithSeed(b, 4096, seed)
8992

90-
pool := bmt.NewPool(bmt.NewConf(swarm.NewHasher, testSegmentCount, testPoolSize))
93+
pool := bmt.NewPool(bmt.NewConf(testSegmentCount, testPoolSize))
9194
h := pool.Get()
9295
defer pool.Put(h)
9396

@@ -106,7 +109,7 @@ func benchmarkPool(b *testing.B, poolsize int) {
106109

107110
testData := testutil.RandBytesWithSeed(b, 4096, seed)
108111

109-
pool := bmt.NewPool(bmt.NewConf(swarm.NewHasher, testSegmentCount, poolsize))
112+
pool := bmt.NewPool(bmt.NewConf(testSegmentCount, poolsize))
110113
cycles := 100
111114

112115
b.ReportAllocs()
@@ -127,6 +130,25 @@ func benchmarkPool(b *testing.B, poolsize int) {
127130
}
128131
}
129132

133+
// benchmarks BMT Hasher with SIMD disabled
134+
func benchmarkBMTNoSIMD(b *testing.B, n int) {
135+
b.Helper()
136+
137+
testData := testutil.RandBytesWithSeed(b, 4096, seed)
138+
139+
pool := bmt.NewPool(bmt.NewConfNoSIMD(testSegmentCount, testPoolSize))
140+
h := pool.Get()
141+
defer pool.Put(h)
142+
143+
b.ReportAllocs()
144+
145+
for b.Loop() {
146+
if _, err := syncHash(h, testData[:n]); err != nil {
147+
b.Fatalf("seed %d: %v", seed, err)
148+
}
149+
}
150+
}
151+
130152
// benchmarks the reference hasher
131153
func benchmarkRefHasher(b *testing.B, n int) {
132154
b.Helper()

pkg/bmt/bmt.go

Lines changed: 0 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,6 @@ var (
1818
zerosection = make([]byte, 64)
1919
)
2020

21-
// Hasher is a reusable hasher for fixed maximum size chunks representing a BMT
22-
// It reuses a pool of trees for amortised memory allocation and resource control,
23-
// and supports order-agnostic concurrent segment writes and section (double segment) writes
24-
// as well as sequential read and write.
25-
//
26-
// The same hasher instance must not be called concurrently on more than one chunk.
27-
//
28-
// The same hasher instance is synchronously reusable.
29-
//
30-
// Sum gives back the tree to the pool and guaranteed to leave
31-
// the tree and itself in a state reusable for hashing a new chunk.
32-
type Hasher struct {
33-
*Conf // configuration
34-
bmt *tree // prebuilt BMT resource for flowcontrol and proofs
35-
size int // bytes written to Hasher since last Reset()
36-
pos int // index of rightmost currently open segment
37-
result chan []byte // result channel
38-
errc chan error // error channel
39-
span []byte // The span of the data subsumed under the chunk
40-
}
41-
42-
// NewHasher gives back an instance of a Hasher struct
43-
func NewHasher(hasherFact func() hash.Hash) *Hasher {
44-
conf := NewConf(hasherFact, swarm.BmtBranches, 32)
45-
46-
return &Hasher{
47-
Conf: conf,
48-
result: make(chan []byte),
49-
errc: make(chan error, 1),
50-
span: make([]byte, SpanSize),
51-
bmt: newTree(conf.maxSize, conf.depth, conf.hasher),
52-
}
53-
}
54-
5521
// Capacity returns the maximum amount of bytes that will be processed by this hasher implementation.
5622
// since BMT assumes a balanced binary tree, capacity it is always a power of 2
5723
func (h *Hasher) Capacity() int {
@@ -91,191 +57,12 @@ func (h *Hasher) BlockSize() int {
9157
return 2 * h.segmentSize
9258
}
9359

94-
// Hash returns the BMT root hash of the buffer and an error
95-
// using Hash presupposes sequential synchronous writes (io.Writer interface).
96-
func (h *Hasher) Hash(b []byte) ([]byte, error) {
97-
if h.size == 0 {
98-
return doHash(h.hasher(), h.span, h.zerohashes[h.depth])
99-
}
100-
copy(h.bmt.buffer[h.size:], zerosection)
101-
// write the last section with final flag set to true
102-
go h.processSection(h.pos, true)
103-
select {
104-
case result := <-h.result:
105-
return doHash(h.hasher(), h.span, result)
106-
case err := <-h.errc:
107-
return nil, err
108-
}
109-
}
110-
11160
// Sum returns the BMT root hash of the buffer, unsafe version of Hash
11261
func (h *Hasher) Sum(b []byte) []byte {
11362
s, _ := h.Hash(b)
11463
return s
11564
}
11665

117-
// Write calls sequentially add to the buffer to be hashed,
118-
// with every full segment calls processSection in a go routine.
119-
func (h *Hasher) Write(b []byte) (int, error) {
120-
l := len(b)
121-
maxVal := h.maxSize - h.size
122-
if l > maxVal {
123-
l = maxVal
124-
}
125-
copy(h.bmt.buffer[h.size:], b)
126-
secsize := 2 * h.segmentSize
127-
from := h.size / secsize
128-
h.size += l
129-
to := h.size / secsize
130-
if l == maxVal {
131-
to--
132-
}
133-
h.pos = to
134-
for i := from; i < to; i++ {
135-
go h.processSection(i, false)
136-
}
137-
return l, nil
138-
}
139-
140-
// Reset prepares the Hasher for reuse
141-
func (h *Hasher) Reset() {
142-
h.pos = 0
143-
h.size = 0
144-
copy(h.span, zerospan)
145-
}
146-
147-
// processSection writes the hash of i-th section into level 1 node of the BMT tree.
148-
func (h *Hasher) processSection(i int, final bool) {
149-
secsize := 2 * h.segmentSize
150-
offset := i * secsize
151-
level := 1
152-
// select the leaf node for the section
153-
n := h.bmt.leaves[i]
154-
isLeft := n.isLeft
155-
hasher := n.hasher
156-
n = n.parent
157-
// hash the section
158-
section, err := doHash(hasher, h.bmt.buffer[offset:offset+secsize])
159-
if err != nil {
160-
select {
161-
case h.errc <- err:
162-
default:
163-
}
164-
return
165-
}
166-
// write hash into parent node
167-
if final {
168-
// for the last segment use writeFinalNode
169-
h.writeFinalNode(level, n, isLeft, section)
170-
} else {
171-
h.writeNode(n, isLeft, section)
172-
}
173-
}
174-
175-
// writeNode pushes the data to the node.
176-
// if it is the first of 2 sisters written, the routine terminates.
177-
// if it is the second, it calculates the hash and writes it
178-
// to the parent node recursively.
179-
// since hashing the parent is synchronous the same hasher can be used.
180-
func (h *Hasher) writeNode(n *node, isLeft bool, s []byte) {
181-
var err error
182-
for {
183-
// at the root of the bmt just write the result to the result channel
184-
if n == nil {
185-
h.result <- s
186-
return
187-
}
188-
// otherwise assign child hash to left or right segment
189-
if isLeft {
190-
n.left = s
191-
} else {
192-
n.right = s
193-
}
194-
// the child-thread first arriving will terminate
195-
if n.toggle() {
196-
return
197-
}
198-
// the thread coming second now can be sure both left and right children are written
199-
// so it calculates the hash of left|right and pushes it to the parent
200-
s, err = doHash(n.hasher, n.left, n.right)
201-
if err != nil {
202-
select {
203-
case h.errc <- err:
204-
default:
205-
}
206-
return
207-
}
208-
isLeft = n.isLeft
209-
n = n.parent
210-
}
211-
}
212-
213-
// writeFinalNode is following the path starting from the final datasegment to the
214-
// BMT root via parents.
215-
// For unbalanced trees it fills in the missing right sister nodes using
216-
// the pool's lookup table for BMT subtree root hashes for all-zero sections.
217-
// Otherwise behaves like `writeNode`.
218-
func (h *Hasher) writeFinalNode(level int, n *node, isLeft bool, s []byte) {
219-
var err error
220-
for {
221-
// at the root of the bmt just write the result to the result channel
222-
if n == nil {
223-
if s != nil {
224-
h.result <- s
225-
}
226-
return
227-
}
228-
var noHash bool
229-
if isLeft {
230-
// coming from left sister branch
231-
// when the final section's path is going via left child node
232-
// we include an all-zero subtree hash for the right level and toggle the node.
233-
n.right = h.zerohashes[level]
234-
if s != nil {
235-
n.left = s
236-
// if a left final node carries a hash, it must be the first (and only thread)
237-
// so the toggle is already in passive state no need no call
238-
// yet thread needs to carry on pushing hash to parent
239-
noHash = false
240-
} else {
241-
// if again first thread then propagate nil and calculate no hash
242-
noHash = n.toggle()
243-
}
244-
} else {
245-
// right sister branch
246-
if s != nil {
247-
// if hash was pushed from right child node, write right segment change state
248-
n.right = s
249-
// if toggle is true, we arrived first so no hashing just push nil to parent
250-
noHash = n.toggle()
251-
} else {
252-
// if s is nil, then thread arrived first at previous node and here there will be two,
253-
// so no need to do anything and keep s = nil for parent
254-
noHash = true
255-
}
256-
}
257-
// the child-thread first arriving will just continue resetting s to nil
258-
// the second thread now can be sure both left and right children are written
259-
// it calculates the hash of left|right and pushes it to the parent
260-
if noHash {
261-
s = nil
262-
} else {
263-
s, err = doHash(n.hasher, n.left, n.right)
264-
if err != nil {
265-
select {
266-
case h.errc <- err:
267-
default:
268-
}
269-
return
270-
}
271-
}
272-
// iterate to parent
273-
isLeft = n.isLeft
274-
n = n.parent
275-
level++
276-
}
277-
}
278-
27966
// calculates the Keccak256 SHA3 hash of the data
28067
func sha3hash(data ...[]byte) ([]byte, error) {
28168
return doHash(swarm.NewHasher(), data...)

0 commit comments

Comments
 (0)