From 9c401f9c9ab5aef77d300b240264fdae2b982474 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Jul 2026 20:48:20 +0900 Subject: [PATCH] Add fast path for single-character patterns in FuzzyMatchV2 For single ASCII character, jump between occurrences with vectorized search and score each directly from bonus matrix instead of running full pipeline with prefilter and score arrays. 2x faster on 100K-line benchmark corpus (5.4ms -> 2.5ms). Matters most on first keystroke where candidate set is largest and result cache cannot help. --- src/algo/algo.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/algo/algo.go b/src/algo/algo.go index 2fddcab3..983c5e82 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -425,6 +425,44 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in } } +// fuzzyMatchV2Single is a fast path for a single-character ASCII pattern on +// ASCII input. Same scoring and tiebreaks as Phase 2 of FuzzyMatchV2, but +// jumps between occurrences instead of scanning every character, and +// allocates no arrays. +func fuzzyMatchV2Single(caseSensitive bool, forward bool, input *util.Chars, b byte, withPos bool) (Result, *[]int) { + byteArray := input.Bytes() + maxScore, maxScorePos := int16(0), -1 + for idx := 0; idx < len(byteArray); { + idx = trySkip(input, caseSensitive, b, idx) + if idx < 0 { + break + } + class := asciiCharClasses[byteArray[idx]] + prevClass := initialCharClass + if idx > 0 { + prevClass = asciiCharClasses[byteArray[idx-1]] + } + bonus := bonusMatrix[prevClass][class] + score := scoreMatch + bonus*bonusFirstCharMultiplier + if forward && score > maxScore || !forward && score >= maxScore { + maxScore, maxScorePos = score, idx + if forward && bonus >= bonusBoundary { + break + } + } + idx++ + } + if maxScorePos < 0 { + return Result{-1, -1, 0}, nil + } + result := Result{maxScorePos, maxScorePos + 1, int(maxScore)} + if !withPos { + return result, nil + } + pos := []int{maxScorePos} + return result, &pos +} + func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { // Assume that pattern is given in lowercase if case-insensitive. // First check if there's a match and calculate bonus for each position. @@ -447,6 +485,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) } + // Single-character ASCII pattern needs neither the prefilter nor the + // score matrix + if M == 1 && input.IsBytes() && pattern[0] < utf8.RuneSelf { + return fuzzyMatchV2Single(caseSensitive, forward, input, byte(pattern[0]), withPos) + } + // Phase 1. Optimized search for ASCII string minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive) if minIdx < 0 {