mirror of
https://github.com/golang/go
synced 2024-11-19 06:34:42 -07:00
3ca1f28e54
There's no point in computing whether we're at the beginning of the line if the NFA isn't going to ask. Wait to compute that until asked. Whatever minor slowdowns were introduced by the conversion to pools that were not repaid by other optimizations are taken care of by this one. name old time/op new time/op delta Find-12 252ns ± 0% 260ns ± 0% +3.34% (p=0.000 n=10+8) FindAllNoMatches-12 136ns ± 4% 134ns ± 4% -0.96% (p=0.033 n=10+10) FindString-12 246ns ± 0% 250ns ± 0% +1.46% (p=0.000 n=8+10) FindSubmatch-12 332ns ± 1% 332ns ± 0% ~ (p=0.101 n=9+10) FindStringSubmatch-12 321ns ± 1% 322ns ± 1% ~ (p=0.717 n=9+10) Literal-12 91.6ns ± 0% 92.3ns ± 0% +0.74% (p=0.000 n=9+9) NotLiteral-12 1.47µs ± 0% 1.47µs ± 0% +0.38% (p=0.000 n=9+8) MatchClass-12 2.15µs ± 0% 2.15µs ± 0% +0.39% (p=0.000 n=10+10) MatchClass_InRange-12 2.09µs ± 0% 2.11µs ± 0% +0.75% (p=0.000 n=9+9) ReplaceAll-12 1.40µs ± 0% 1.40µs ± 0% ~ (p=0.525 n=10+10) AnchoredLiteralShortNonMatch-12 83.5ns ± 0% 81.6ns ± 0% -2.28% (p=0.000 n=9+10) AnchoredLiteralLongNonMatch-12 101ns ± 0% 97ns ± 1% -3.54% (p=0.000 n=10+10) AnchoredShortMatch-12 131ns ± 0% 128ns ± 0% -2.29% (p=0.000 n=10+9) AnchoredLongMatch-12 268ns ± 1% 252ns ± 1% -6.04% (p=0.000 n=10+10) OnePassShortA-12 614ns ± 0% 587ns ± 1% -4.33% (p=0.000 n=6+10) NotOnePassShortA-12 552ns ± 0% 547ns ± 1% -0.89% (p=0.000 n=10+10) OnePassShortB-12 494ns ± 0% 455ns ± 0% -7.96% (p=0.000 n=9+9) NotOnePassShortB-12 411ns ± 0% 406ns ± 0% -1.30% (p=0.000 n=9+9) OnePassLongPrefix-12 109ns ± 0% 108ns ± 1% ~ (p=0.064 n=8+9) OnePassLongNotPrefix-12 403ns ± 0% 349ns ± 0% -13.30% (p=0.000 n=9+8) MatchParallelShared-12 38.9ns ± 1% 37.9ns ± 1% -2.65% (p=0.000 n=10+8) MatchParallelCopied-12 39.2ns ± 1% 38.3ns ± 2% -2.20% (p=0.001 n=10+10) QuoteMetaAll-12 94.5ns ± 0% 94.7ns ± 0% +0.18% (p=0.043 n=10+9) QuoteMetaNone-12 52.7ns ± 0% 52.7ns ± 0% ~ (all equal) Match/Easy0/32-12 72.2ns ± 0% 71.9ns ± 0% -0.38% (p=0.009 n=8+10) Match/Easy0/1K-12 296ns ± 1% 297ns ± 0% +0.51% (p=0.001 n=10+9) Match/Easy0/32K-12 4.57µs ± 3% 4.61µs ± 2% ~ (p=0.280 n=10+10) Match/Easy0/1M-12 234µs ± 0% 234µs ± 0% ~ (p=0.986 n=10+10) Match/Easy0/32M-12 7.96ms ± 0% 7.98ms ± 0% +0.22% (p=0.010 n=10+9) Match/Easy0i/32-12 1.09µs ± 0% 1.10µs ± 0% +0.23% (p=0.000 n=8+9) Match/Easy0i/1K-12 31.7µs ± 0% 31.7µs ± 0% +0.09% (p=0.003 n=9+8) Match/Easy0i/32K-12 1.61ms ± 0% 1.27ms ± 1% -21.03% (p=0.000 n=8+10) Match/Easy0i/1M-12 51.4ms ± 0% 40.4ms ± 0% -21.29% (p=0.000 n=8+8) Match/Easy0i/32M-12 1.65s ± 0% 1.30s ± 1% -21.22% (p=0.000 n=9+9) Match/Easy1/32-12 67.6ns ± 1% 67.2ns ± 0% ~ (p=0.085 n=10+9) Match/Easy1/1K-12 873ns ± 2% 880ns ± 0% +0.78% (p=0.006 n=9+7) Match/Easy1/32K-12 39.7µs ± 1% 34.3µs ± 3% -13.53% (p=0.000 n=10+10) Match/Easy1/1M-12 1.41ms ± 1% 1.19ms ± 3% -15.48% (p=0.000 n=10+10) Match/Easy1/32M-12 44.9ms ± 1% 38.0ms ± 2% -15.21% (p=0.000 n=10+10) Match/Medium/32-12 1.04µs ± 0% 1.03µs ± 0% -0.57% (p=0.000 n=9+9) Match/Medium/1K-12 31.2µs ± 0% 31.4µs ± 1% +0.61% (p=0.000 n=8+10) Match/Medium/32K-12 1.45ms ± 1% 1.20ms ± 0% -17.70% (p=0.000 n=10+8) Match/Medium/1M-12 46.4ms ± 0% 38.4ms ± 2% -17.32% (p=0.000 n=6+9) Match/Medium/32M-12 1.49s ± 1% 1.24s ± 1% -16.81% (p=0.000 n=10+10) Match/Hard/32-12 1.47µs ± 0% 1.47µs ± 0% -0.31% (p=0.000 n=9+10) Match/Hard/1K-12 44.5µs ± 1% 44.4µs ± 0% ~ (p=0.075 n=10+10) Match/Hard/32K-12 2.09ms ± 0% 1.78ms ± 7% -14.88% (p=0.000 n=8+10) Match/Hard/1M-12 67.8ms ± 5% 56.9ms ± 7% -16.05% (p=0.000 n=10+10) Match/Hard/32M-12 2.17s ± 5% 1.84s ± 6% -15.21% (p=0.000 n=10+10) Match/Hard1/32-12 7.89µs ± 0% 7.94µs ± 0% +0.61% (p=0.000 n=9+9) Match/Hard1/1K-12 246µs ± 0% 245µs ± 0% -0.30% (p=0.010 n=9+10) Match/Hard1/32K-12 8.93ms ± 0% 8.17ms ± 0% -8.44% (p=0.000 n=9+8) Match/Hard1/1M-12 286ms ± 0% 269ms ± 9% -5.66% (p=0.028 n=9+10) Match/Hard1/32M-12 9.16s ± 0% 8.61s ± 8% -5.98% (p=0.028 n=9+10) Match_onepass_regex/32-12 825ns ± 0% 712ns ± 0% -13.75% (p=0.000 n=8+8) Match_onepass_regex/1K-12 28.7µs ± 1% 19.8µs ± 0% -30.99% (p=0.000 n=9+8) Match_onepass_regex/32K-12 950µs ± 1% 628µs ± 0% -33.83% (p=0.000 n=9+8) Match_onepass_regex/1M-12 30.4ms ± 0% 20.1ms ± 0% -33.74% (p=0.000 n=9+8) Match_onepass_regex/32M-12 974ms ± 1% 646ms ± 0% -33.73% (p=0.000 n=9+8) CompileOnepass-12 4.60µs ± 0% 4.59µs ± 0% ~ (p=0.063 n=8+9) [Geo mean] 23.1µs 21.3µs -7.44% https://perf.golang.org/search?q=upload:20181004.4 Change-Id: I47cdd09f6dcde1d7c317080e9b4df42c7d0a8d24 Reviewed-on: https://go-review.googlesource.com/c/139782 Run-TryBot: Russ Cox <rsc@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
368 lines
8.8 KiB
Go
368 lines
8.8 KiB
Go
// Copyright 2015 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// backtrack is a regular expression search with submatch
|
|
// tracking for small regular expressions and texts. It allocates
|
|
// a bit vector with (length of input) * (length of prog) bits,
|
|
// to make sure it never explores the same (character position, instruction)
|
|
// state multiple times. This limits the search to run in time linear in
|
|
// the length of the test.
|
|
//
|
|
// backtrack is a fast replacement for the NFA code on small
|
|
// regexps when onepass cannot be used.
|
|
|
|
package regexp
|
|
|
|
import (
|
|
"regexp/syntax"
|
|
"sync"
|
|
)
|
|
|
|
// A job is an entry on the backtracker's job stack. It holds
|
|
// the instruction pc and the position in the input.
|
|
type job struct {
|
|
pc uint32
|
|
arg bool
|
|
pos int
|
|
}
|
|
|
|
const (
|
|
visitedBits = 32
|
|
maxBacktrackProg = 500 // len(prog.Inst) <= max
|
|
maxBacktrackVector = 256 * 1024 // bit vector size <= max (bits)
|
|
)
|
|
|
|
// bitState holds state for the backtracker.
|
|
type bitState struct {
|
|
end int
|
|
cap []int
|
|
matchcap []int
|
|
jobs []job
|
|
visited []uint32
|
|
|
|
inputs inputs
|
|
}
|
|
|
|
var bitStatePool sync.Pool
|
|
|
|
func newBitState() *bitState {
|
|
b, ok := bitStatePool.Get().(*bitState)
|
|
if !ok {
|
|
b = new(bitState)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func freeBitState(b *bitState) {
|
|
b.inputs.clear()
|
|
bitStatePool.Put(b)
|
|
}
|
|
|
|
// maxBitStateLen returns the maximum length of a string to search with
|
|
// the backtracker using prog.
|
|
func maxBitStateLen(prog *syntax.Prog) int {
|
|
if !shouldBacktrack(prog) {
|
|
return 0
|
|
}
|
|
return maxBacktrackVector / len(prog.Inst)
|
|
}
|
|
|
|
// shouldBacktrack reports whether the program is too
|
|
// long for the backtracker to run.
|
|
func shouldBacktrack(prog *syntax.Prog) bool {
|
|
return len(prog.Inst) <= maxBacktrackProg
|
|
}
|
|
|
|
// reset resets the state of the backtracker.
|
|
// end is the end position in the input.
|
|
// ncap is the number of captures.
|
|
func (b *bitState) reset(prog *syntax.Prog, end int, ncap int) {
|
|
b.end = end
|
|
|
|
if cap(b.jobs) == 0 {
|
|
b.jobs = make([]job, 0, 256)
|
|
} else {
|
|
b.jobs = b.jobs[:0]
|
|
}
|
|
|
|
visitedSize := (len(prog.Inst)*(end+1) + visitedBits - 1) / visitedBits
|
|
if cap(b.visited) < visitedSize {
|
|
b.visited = make([]uint32, visitedSize, maxBacktrackVector/visitedBits)
|
|
} else {
|
|
b.visited = b.visited[:visitedSize]
|
|
for i := range b.visited {
|
|
b.visited[i] = 0
|
|
}
|
|
}
|
|
|
|
if cap(b.cap) < ncap {
|
|
b.cap = make([]int, ncap)
|
|
} else {
|
|
b.cap = b.cap[:ncap]
|
|
}
|
|
for i := range b.cap {
|
|
b.cap[i] = -1
|
|
}
|
|
|
|
if cap(b.matchcap) < ncap {
|
|
b.matchcap = make([]int, ncap)
|
|
} else {
|
|
b.matchcap = b.matchcap[:ncap]
|
|
}
|
|
for i := range b.matchcap {
|
|
b.matchcap[i] = -1
|
|
}
|
|
}
|
|
|
|
// shouldVisit reports whether the combination of (pc, pos) has not
|
|
// been visited yet.
|
|
func (b *bitState) shouldVisit(pc uint32, pos int) bool {
|
|
n := uint(int(pc)*(b.end+1) + pos)
|
|
if b.visited[n/visitedBits]&(1<<(n&(visitedBits-1))) != 0 {
|
|
return false
|
|
}
|
|
b.visited[n/visitedBits] |= 1 << (n & (visitedBits - 1))
|
|
return true
|
|
}
|
|
|
|
// push pushes (pc, pos, arg) onto the job stack if it should be
|
|
// visited.
|
|
func (b *bitState) push(re *Regexp, pc uint32, pos int, arg bool) {
|
|
// Only check shouldVisit when arg is false.
|
|
// When arg is true, we are continuing a previous visit.
|
|
if re.prog.Inst[pc].Op != syntax.InstFail && (arg || b.shouldVisit(pc, pos)) {
|
|
b.jobs = append(b.jobs, job{pc: pc, arg: arg, pos: pos})
|
|
}
|
|
}
|
|
|
|
// tryBacktrack runs a backtracking search starting at pos.
|
|
func (re *Regexp) tryBacktrack(b *bitState, i input, pc uint32, pos int) bool {
|
|
longest := re.longest
|
|
|
|
b.push(re, pc, pos, false)
|
|
for len(b.jobs) > 0 {
|
|
l := len(b.jobs) - 1
|
|
// Pop job off the stack.
|
|
pc := b.jobs[l].pc
|
|
pos := b.jobs[l].pos
|
|
arg := b.jobs[l].arg
|
|
b.jobs = b.jobs[:l]
|
|
|
|
// Optimization: rather than push and pop,
|
|
// code that is going to Push and continue
|
|
// the loop simply updates ip, p, and arg
|
|
// and jumps to CheckAndLoop. We have to
|
|
// do the ShouldVisit check that Push
|
|
// would have, but we avoid the stack
|
|
// manipulation.
|
|
goto Skip
|
|
CheckAndLoop:
|
|
if !b.shouldVisit(pc, pos) {
|
|
continue
|
|
}
|
|
Skip:
|
|
|
|
inst := re.prog.Inst[pc]
|
|
|
|
switch inst.Op {
|
|
default:
|
|
panic("bad inst")
|
|
case syntax.InstFail:
|
|
panic("unexpected InstFail")
|
|
case syntax.InstAlt:
|
|
// Cannot just
|
|
// b.push(inst.Out, pos, false)
|
|
// b.push(inst.Arg, pos, false)
|
|
// If during the processing of inst.Out, we encounter
|
|
// inst.Arg via another path, we want to process it then.
|
|
// Pushing it here will inhibit that. Instead, re-push
|
|
// inst with arg==true as a reminder to push inst.Arg out
|
|
// later.
|
|
if arg {
|
|
// Finished inst.Out; try inst.Arg.
|
|
arg = false
|
|
pc = inst.Arg
|
|
goto CheckAndLoop
|
|
} else {
|
|
b.push(re, pc, pos, true)
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
}
|
|
|
|
case syntax.InstAltMatch:
|
|
// One opcode consumes runes; the other leads to match.
|
|
switch re.prog.Inst[inst.Out].Op {
|
|
case syntax.InstRune, syntax.InstRune1, syntax.InstRuneAny, syntax.InstRuneAnyNotNL:
|
|
// inst.Arg is the match.
|
|
b.push(re, inst.Arg, pos, false)
|
|
pc = inst.Arg
|
|
pos = b.end
|
|
goto CheckAndLoop
|
|
}
|
|
// inst.Out is the match - non-greedy
|
|
b.push(re, inst.Out, b.end, false)
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
|
|
case syntax.InstRune:
|
|
r, width := i.step(pos)
|
|
if !inst.MatchRune(r) {
|
|
continue
|
|
}
|
|
pos += width
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
|
|
case syntax.InstRune1:
|
|
r, width := i.step(pos)
|
|
if r != inst.Rune[0] {
|
|
continue
|
|
}
|
|
pos += width
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
|
|
case syntax.InstRuneAnyNotNL:
|
|
r, width := i.step(pos)
|
|
if r == '\n' || r == endOfText {
|
|
continue
|
|
}
|
|
pos += width
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
|
|
case syntax.InstRuneAny:
|
|
r, width := i.step(pos)
|
|
if r == endOfText {
|
|
continue
|
|
}
|
|
pos += width
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
|
|
case syntax.InstCapture:
|
|
if arg {
|
|
// Finished inst.Out; restore the old value.
|
|
b.cap[inst.Arg] = pos
|
|
continue
|
|
} else {
|
|
if 0 <= inst.Arg && inst.Arg < uint32(len(b.cap)) {
|
|
// Capture pos to register, but save old value.
|
|
b.push(re, pc, b.cap[inst.Arg], true) // come back when we're done.
|
|
b.cap[inst.Arg] = pos
|
|
}
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
}
|
|
|
|
case syntax.InstEmptyWidth:
|
|
flag := i.context(pos)
|
|
if !flag.match(syntax.EmptyOp(inst.Arg)) {
|
|
continue
|
|
}
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
|
|
case syntax.InstNop:
|
|
pc = inst.Out
|
|
goto CheckAndLoop
|
|
|
|
case syntax.InstMatch:
|
|
// We found a match. If the caller doesn't care
|
|
// where the match is, no point going further.
|
|
if len(b.cap) == 0 {
|
|
return true
|
|
}
|
|
|
|
// Record best match so far.
|
|
// Only need to check end point, because this entire
|
|
// call is only considering one start position.
|
|
if len(b.cap) > 1 {
|
|
b.cap[1] = pos
|
|
}
|
|
if old := b.matchcap[1]; old == -1 || (longest && pos > 0 && pos > old) {
|
|
copy(b.matchcap, b.cap)
|
|
}
|
|
|
|
// If going for first match, we're done.
|
|
if !longest {
|
|
return true
|
|
}
|
|
|
|
// If we used the entire text, no longer match is possible.
|
|
if pos == b.end {
|
|
return true
|
|
}
|
|
|
|
// Otherwise, continue on in hope of a longer match.
|
|
continue
|
|
}
|
|
}
|
|
|
|
return longest && len(b.matchcap) > 1 && b.matchcap[1] >= 0
|
|
}
|
|
|
|
// backtrack runs a backtracking search of prog on the input starting at pos.
|
|
func (re *Regexp) backtrack(ib []byte, is string, pos int, ncap int, dstCap []int) []int {
|
|
startCond := re.cond
|
|
if startCond == ^syntax.EmptyOp(0) { // impossible
|
|
return nil
|
|
}
|
|
if startCond&syntax.EmptyBeginText != 0 && pos != 0 {
|
|
// Anchored match, past beginning of text.
|
|
return nil
|
|
}
|
|
|
|
b := newBitState()
|
|
i, end := b.inputs.init(nil, ib, is)
|
|
b.reset(re.prog, end, ncap)
|
|
|
|
// Anchored search must start at the beginning of the input
|
|
if startCond&syntax.EmptyBeginText != 0 {
|
|
if len(b.cap) > 0 {
|
|
b.cap[0] = pos
|
|
}
|
|
if !re.tryBacktrack(b, i, uint32(re.prog.Start), pos) {
|
|
freeBitState(b)
|
|
return nil
|
|
}
|
|
} else {
|
|
|
|
// Unanchored search, starting from each possible text position.
|
|
// Notice that we have to try the empty string at the end of
|
|
// the text, so the loop condition is pos <= end, not pos < end.
|
|
// This looks like it's quadratic in the size of the text,
|
|
// but we are not clearing visited between calls to TrySearch,
|
|
// so no work is duplicated and it ends up still being linear.
|
|
width := -1
|
|
for ; pos <= end && width != 0; pos += width {
|
|
if len(re.prefix) > 0 {
|
|
// Match requires literal prefix; fast search for it.
|
|
advance := i.index(re, pos)
|
|
if advance < 0 {
|
|
freeBitState(b)
|
|
return nil
|
|
}
|
|
pos += advance
|
|
}
|
|
|
|
if len(b.cap) > 0 {
|
|
b.cap[0] = pos
|
|
}
|
|
if re.tryBacktrack(b, i, uint32(re.prog.Start), pos) {
|
|
// Match must be leftmost; done.
|
|
goto Match
|
|
}
|
|
_, width = i.step(pos)
|
|
}
|
|
freeBitState(b)
|
|
return nil
|
|
}
|
|
|
|
Match:
|
|
dstCap = append(dstCap, b.matchcap...)
|
|
freeBitState(b)
|
|
return dstCap
|
|
}
|