Add direct algo fast path in matchChunk

For the common case of a single fuzzy term with no nth transform,
call the algo function directly from matchChunk, bypassing the
MatchItem -> extendedMatch -> iter dispatch chain. This eliminates
3 function calls and the per-match []Offset heap allocation.
This commit is contained in:
Junegunn Choi
2026-02-28 15:03:30 +09:00
parent 8452c78cc8
commit 3e751c4e87
2 changed files with 56 additions and 3 deletions

View File

@@ -65,6 +65,8 @@ type Pattern struct {
cache *ChunkCache cache *ChunkCache
denylist map[int32]struct{} denylist map[int32]struct{}
startIndex int32 startIndex int32
directAlgo algo.Algo
directTerm *term
} }
var _splitRegex *regexp.Regexp var _splitRegex *regexp.Regexp
@@ -151,6 +153,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)
ptr.procFun[termFuzzy] = fuzzyAlgo ptr.procFun[termFuzzy] = fuzzyAlgo
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
ptr.procFun[termExact] = algo.ExactMatchNaive ptr.procFun[termExact] = algo.ExactMatchNaive
@@ -274,6 +277,22 @@ func (p *Pattern) buildCacheKey() string {
return strings.Join(cacheableTerms, "\t") return strings.Join(cacheableTerms, "\t")
} }
// buildDirectAlgo returns the algo function and term for the direct fast path
// in matchChunk. Returns (nil, nil) if the pattern is not suitable.
// Requirements: extended mode, single term set with single non-inverse fuzzy term, no nth.
func (p *Pattern) buildDirectAlgo(fuzzyAlgo algo.Algo) (algo.Algo, *term) {
if !p.extended || len(p.nth) > 0 {
return nil, nil
}
if len(p.termSets) == 1 && len(p.termSets[0]) == 1 {
t := &p.termSets[0][0]
if !t.inv && t.typ == termFuzzy {
return fuzzyAlgo, t
}
}
return nil, nil
}
// CacheKey is used to build string to be used as the key of result cache // CacheKey is used to build string to be used as the key of result cache
func (p *Pattern) CacheKey() string { func (p *Pattern) CacheKey() string {
return p.cacheKey return p.cacheKey
@@ -312,6 +331,35 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
} }
} }
// Fast path: single fuzzy term, no nth, no denylist.
// Calls the algo function directly, bypassing MatchItem/extendedMatch/iter
// and avoiding per-match []Offset heap allocation.
if p.directAlgo != nil && len(p.denylist) == 0 {
t := p.directTerm
if space == nil {
for idx := startIdx; idx < chunk.count; idx++ {
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
&chunk.items[idx].text, t.text, p.withPos, slab)
if res.Start >= 0 {
matches = append(matches, buildResultFromBounds(
&chunk.items[idx], res.Score,
int(res.Start), int(res.End), int(res.End), true))
}
}
} else {
for _, result := range space {
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
&result.item.text, t.text, p.withPos, slab)
if res.Start >= 0 {
matches = append(matches, buildResultFromBounds(
result.item, res.Score,
int(res.Start), int(res.End), int(res.End), true))
}
}
}
return matches
}
if len(p.denylist) == 0 { if len(p.denylist) == 0 {
// Huge code duplication for minimizing unnecessary map lookups // Huge code duplication for minimizing unnecessary map lookups
if space == nil { if space == nil {

View File

@@ -33,8 +33,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
} }
result := Result{item: item}
numChars := item.text.Length()
minBegin := math.MaxUint16 minBegin := math.MaxUint16
minEnd := math.MaxUint16 minEnd := math.MaxUint16
maxEnd := 0 maxEnd := 0
@@ -49,6 +47,14 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
} }
} }
return buildResultFromBounds(item, score, minBegin, minEnd, maxEnd, validOffsetFound)
}
// buildResultFromBounds builds a Result from pre-computed offset bounds.
func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int, validOffsetFound bool) Result {
result := Result{item: item}
numChars := item.text.Length()
for idx, criterion := range sortCriteria { for idx, criterion := range sortCriteria {
val := uint16(math.MaxUint16) val := uint16(math.MaxUint16)
switch criterion { switch criterion {
@@ -75,7 +81,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
val = item.TrimLength() val = item.TrimLength()
case byPathname: case byPathname:
if validOffsetFound { if validOffsetFound {
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
lastDelim := -1 lastDelim := -1
s := item.text.ToString() s := item.text.ToString()
for i := len(s) - 1; i >= 0; i-- { for i := len(s) - 1; i >= 0; i-- {