Skip to content

Commit 39e35f1

Browse files
authored
perf: optimize String.charactersIn (#14395)
This PR attempts to make `String.charactersIn` faster by doing less work (and in particular only `USize` and `UInt8` arithmetic, not `Nat`) in the hot loop.
1 parent 47cd72d commit 39e35f1

3 files changed

Lines changed: 55 additions & 18 deletions

File tree

src/Init/Data/UInt/Basic.lean

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,15 @@ def UInt8.toAsciiLower (b : UInt8) : UInt8 :=
190190
-- to generate slighly better assembly.
191191
b + ((decide (b - 65 < 26)).toUInt8 <<< 5)
192192

193+
/--
194+
If `b` is the ASCII value of a lowercase character return the corresponding
195+
uppercase value, otherwise leave it untouched.
196+
-/
197+
@[inline]
198+
def UInt8.toAsciiUpper (b : UInt8) : UInt8 :=
199+
-- See comment on `toAsciiLower` above.
200+
b - ((decide (b - 97 < 26)).toUInt8 <<< 5)
201+
193202
/-- Converts a `Fin UInt16.size` into the corresponding `UInt16`. -/
194203
@[inline] def UInt16.ofFin (a : Fin UInt16.size) : UInt16 := ⟨⟨a⟩⟩
195204

src/Init/Data/UInt/Lemmas.lean

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3596,3 +3596,9 @@ theorem UInt64.lt_add_one {c : UInt64} (h : c ≠ -1) : c < c + 1 :=
35963596
theorem USize.lt_add_one {c : USize} (h : c ≠ -1) : c < c + 1 :=
35973597
USize.lt_iff_toBitVec_lt.2 (BitVec.lt_add_one
35983598
(by simpa [← USize.toBitVec_inj, BitVec.neg_one_eq_allOnes] using h))
3599+
3600+
theorem UInt8.toNat_ofNat_le {n : Nat} : (UInt8.ofNat n).toNat ≤ n := toNat_ofNat ▸ Nat.mod_le ..
3601+
theorem UInt16.toNat_ofNat_le {n : Nat} : (UInt16.ofNat n).toNat ≤ n := toNat_ofNat ▸ Nat.mod_le ..
3602+
theorem UInt32.toNat_ofNat_le {n : Nat} : (UInt32.ofNat n).toNat ≤ n := toNat_ofNat ▸ Nat.mod_le ..
3603+
theorem UInt64.toNat_ofNat_le {n : Nat} : (UInt64.ofNat n).toNat ≤ n := toNat_ofNat ▸ Nat.mod_le ..
3604+
theorem USize.toNat_ofNat_le {n : Nat} : (USize.ofNat n).toNat ≤ n := toNat_ofNat ▸ Nat.mod_le ..

src/Lean/Server/Completion/CompletionUtils.lean

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,55 @@ module
77

88
prelude
99
public import Lean.Meta.WHNF
10+
import Init.Data.String.Bootstrap
11+
import Init.Data.UInt.Lemmas
1012

1113
public section
1214

15+
/--
16+
Returns `true` if the characters of `a` occur in `b` in order (not necessarily contiguously),
17+
compared case-insensitively via `Char.toLower`.
18+
19+
This function assumes that `a` and `b` have size at most `USize.size`, which should be a safe
20+
assumption.
21+
-/
1322
partial def Lean.String.charactersIn (a b : String) : Bool :=
14-
goFastScalar 0⟩ ⟨0
23+
goFastScalar a.utf8ByteSize.toUSize b.utf8ByteSize.toUSize 0 0 rfl rfl
1524
where
1625
/-
17-
This function is the ASCII fast path for `go`
26+
This function is the ASCII fast path for `go`. It caches the two case variants of the current
27+
pattern byte in `lower`/`upper` (via `scan`) so that the hot loop scanning `b` only performs a
28+
bounds check, a byte load, and two comparisons per byte.
1829
-/
19-
goFastScalar (aPos bPos : String.Pos.Raw) : Bool :=
20-
if ha : ¬aPos < a.rawEndPos then
21-
true
22-
else if hb : ¬bPos < b.rawEndPos then
23-
false
30+
goFastScalar (aSize bSize aPos bPos : USize)
31+
(ha : aSize = a.utf8ByteSize.toUSize) (hb : bSize = b.utf8ByteSize.toUSize) : Bool :=
32+
if haPos : aPos < aSize then
33+
let aByte := String.Internal.ugetUTF8Byte a aPos
34+
(Std.lt_of_lt_of_le (USize.lt_iff_toNat_lt.1 haPos) (ha ▸ USize.toNat_ofNat_le))
35+
-- If `a` contains a non-ASCII byte we give up on the fast path
36+
if aByte &&& 0x80 != 0 then
37+
go ⟨aPos.toNat⟩ ⟨bPos.toNat⟩
38+
else
39+
scan aByte.toAsciiLower aByte.toAsciiUpper aSize bSize aPos bPos ha hb
2440
else
25-
let aByte := a.getUTF8Byte aPos (by simpa using ha)
26-
let bByte := b.getUTF8Byte bPos (by simpa using hb)
27-
-- If a or b are not UTF-8 bytes we give up on the fast path
28-
if (aByte &&& 0x80 != 0) || (bByte &&& 0x80 != 0) then
29-
go aPos bPos
41+
true
42+
43+
scan (lower upper : UInt8) (aSize bSize aPos bPos : USize)
44+
(ha : aSize = a.utf8ByteSize.toUSize) (hb : bSize = b.utf8ByteSize.toUSize) : Bool :=
45+
if hbPos : bPos < bSize then
46+
let bByte := String.Internal.ugetUTF8Byte b bPos
47+
(Std.lt_of_lt_of_le (USize.lt_iff_toNat_lt.1 hbPos) (hb ▸ USize.toNat_ofNat_le))
48+
-- `lower` and `upper` are ASCII, so a non-ASCII byte can never match and it is fine to
49+
-- check for a match first
50+
if bByte == lower || bByte == upper then
51+
goFastScalar aSize bSize (aPos + 1) (bPos + 1) ha hb
52+
-- If `b` contains a non-ASCII byte we give up on the fast path
53+
else if bByte &&& 0x80 != 0 then
54+
go ⟨aPos.toNat⟩ ⟨bPos.toNat⟩
3055
else
31-
let bPos := ⟨bPos.byteIdx + 1
32-
if aByte.toAsciiLower == bByte.toAsciiLower then
33-
let aPos := ⟨aPos.byteIdx + 1
34-
goFastScalar aPos bPos
35-
else
36-
goFastScalar aPos bPos
56+
scan lower upper aSize bSize aPos (bPos + 1) ha hb
57+
else
58+
false
3759

3860
go (aPos bPos : String.Pos.Raw) : Bool :=
3961
if ha : aPos.atEnd a then

0 commit comments

Comments
 (0)