Faster megular expressions - #265
Conversation
| generation int | ||
| code int | ||
| capture captureFunc | ||
| next int |
There was a problem hiding this comment.
use int64, int is implementation dependent. The bitwise calculations won't work for 32bit machines.
There was a problem hiding this comment.
I'm not relying on int64 bitwise calculations here. Maybe you're thinking of the
visited bitset which is explicitly int64. That's used to flip the bit of when
we've visited a matchstate in the array by its index. And it's fine if that
index is int32 or int64.
|
Changes LGTM! |
| stack = append(stack, task{splitIdx, t.cap}) | ||
| stack = append(stack, task{s.next, t.cap}) |
There was a problem hiding this comment.
IIRC, this was using recursion before and an explicit stack now.
How much better is the stack compared to recursion here?
There was a problem hiding this comment.
Recursive
goos: darwin
goarch: arm64
pkg: github.com/multiformats/go-multiaddr/x/meg
cpu: Apple M1 Max
BenchmarkIsWebTransportMultiaddrPrealloc-10 2778994 429.7 ns/op 160 B/op 9 allocs/op
BenchmarkIsWebTransportMultiaddrNoCapturePrealloc-10 5164788 231.8 ns/op 0 B/op 0 allocs/op
BenchmarkIsWebTransportMultiaddrNoCapture-10 2945781 414.0 ns/op 472 B/op 2 allocs/op
BenchmarkIsWebTransportMultiaddr-10 1296012 919.5 ns/op 920 B/op 25 allocs/op
Non-recursive
BenchmarkIsWebTransportMultiaddrPrealloc-10 3170400 380.5 ns/op 160 B/op 9 allocs/op
BenchmarkIsWebTransportMultiaddrNoCapturePrealloc-10 6977448 171.9 ns/op 0 B/op 0 allocs/op
BenchmarkIsWebTransportMultiaddrNoCapture-10 3290984 353.9 ns/op 472 B/op 2 allocs/op
BenchmarkIsWebTransportMultiaddr-10 1374780 863.3 ns/op 920 B/op 25 allocs/op
This is the recursive implementation:
func appendStateRecursive(arr statesAndCaptures, states []MatchState, stateIndex int, c *capture, visitedBitSet []uint64) statesAndCaptures {
if stateIndex >= len(states) {
return arr
}
s := states[stateIndex]
if visitedBitSet[stateIndex/64]&(1<<(stateIndex%64)) != 0 {
return arr
}
visitedBitSet[stateIndex/64] |= 1 << (stateIndex % 64)
if s.codeOrKind < done {
arr = appendStateRecursive(arr, states, s.next, c, visitedBitSet)
arr = appendStateRecursive(arr, states, restoreSplitIdx(s.codeOrKind), c, visitedBitSet)
} else {
arr.states = append(arr.states, stateIndex)
arr.captures = append(arr.captures, c)
}
return arr
}It's a bit faster, but if you prefer the simplicity of the recursive approach I'm happy to change it back.
There was a problem hiding this comment.
I do prefer the simplicity of the recursive approach
|
It would be interesting to have a benchmark for a multiaddr to String and then using a stdlib string regex on it. |
|
Okay this is much slower: |
* much cheaper copies of captures * Add a benchmark * allocate to a slice. Use indexes as handles * cleanup * Add nocapture loop benchmark It's really fast. No surprise * cleanup * nits
* Add multiaddr expression group matching Support captures export some things wip thinking about public API Think about exposing meg as a public API doc comments Finish rename Add helper for meg and add test add comment for devs * Move meg package to /x/ for experimental * Add fuzz cases * faster Code() method twice as fast without the copy * avoid copying an empty map if no captures * Faster megular expressions (#265) * much cheaper copies of captures * Add a benchmark * allocate to a slice. Use indexes as handles * cleanup * Add nocapture loop benchmark It's really fast. No surprise * cleanup * nits * feat(x/meg): Support capturing components (#269) * Use Matchable interface * Add Bytes to Matchable interface * feat(x/meg): Support capturing bytes * Export CaptureWithF Can be used by more specific capturers (e.g capture net.AddrIP) * Support Any match, RawValue, and multiple Concatenations * Add CaptureAddrPort * Fix typo in rebase * workaround for go generics * less hacky workaround for go generics * Avoid unnecessary copy * Make `matchAny` less greedy when there are alternatives The state machine handling was modified to deprioritize the `matchAny` path when alternatives exist, resulting in less greedy matching behavior when using Any patterns. * PR comments * Update Pattern type to return next state
This PR stills needs some cleanup but sharing early to get some feedback.
This is overall 4x faster than the original PR for the normal case (no preallocation).
The main changes are:
Protocoltype. (I feel like this should have been optimized by the compiler though)MatchStates and Index handles:
By allocating to a single slice rather than the heap, we can prepare the memory upfront and reduce our overall memory pressure. We also make the
MatchStatemuch smaller (24 bytes from 48 bytes). The smaller size and having the states in contiguous memory enable it to be more cache friendly.On my machine, with these changes, using Megular expressions normally is about as fast as the
ForEachstyle in v0.14.Benchmark Results
Assume this system unless otherwise noted:
go-multiaddr @ v0.14 using
.ForEachhttps://github.com/multiformats/go-multiaddr/blob/marco/v0.14.0-bench/bench_test.go:The original megular expressions PR:
All but the Index handles change (the first two points from above):
With the index handles change