Skip to content

Commit dc7d6e5

Browse files
mszabo-wikiasjamesr
authored andcommitted
Add fast path for ASCII case folding
One of our production services uses re2j to match several hundred mostly case-insensitive patterns of varying complexity against text. We observed that approximately 12% of CPU time was being spent in toLowerCase() as called from simpleFold(), due to the necessity of doing at least one character data lookup per Inst.Rune in the common case that the input rune being examined did not match the instruction. As a fix, implement a method equalsIgnoreCase() that performs Unicode-aware case-insensitive comparison between two runes, with a fast path for the common case where both input runes are ASCII, and use it in Inst for single-rune case-insensitive comparison. This takes character data lookups out of the hot path. The existing re2j benchmarks did not exercise case-insensitive patterns, so add a new benchmark that executes a mostly ASCII regex pattern on a text containing a mix of ASCII and Unicode characters (generated using a Hungarian "lorem ipsum" text generator). Also add unit tests for the new equality comparison logic. Signed-off-by: Máté Szabó <[email protected]>
1 parent 3e685d9 commit dc7d6e5

File tree

8 files changed

+306
-34
lines changed

8 files changed

+306
-34
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) 2022 The Go Authors. All rights reserved.
3+
*
4+
* Use of this source code is governed by a BSD-style
5+
* license that can be found in the LICENSE file.
6+
*/
7+
package com.google.re2j.benchmark;
8+
9+
import org.openjdk.jmh.annotations.*;
10+
import org.openjdk.jmh.infra.Blackhole;
11+
12+
import java.nio.charset.StandardCharsets;
13+
import java.util.concurrent.TimeUnit;
14+
15+
// BenchmarkCaseInsensitiveSubmatch tests the performance of case-insensitive matching
16+
// by testing a mostly ASCII regex pattern versus a moderately large text containing both
17+
// ASCII and Unicode characters.
18+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
19+
@State(Scope.Benchmark)
20+
public class BenchmarkCaseInsensitiveSubmatch {
21+
@Param({"JDK", "RE2J"})
22+
private Implementations impl;
23+
24+
@Param({"true", "false"})
25+
private boolean binary;
26+
27+
private final byte[] bytes = BenchmarkUtils.readResourceFile("unicode-sample-text.txt");
28+
29+
private final String text = new String(bytes, StandardCharsets.UTF_8);
30+
31+
private Implementations.Pattern pattern;
32+
33+
@Setup
34+
public void setup() {
35+
pattern =
36+
Implementations.Pattern.compile(
37+
impl,
38+
"(prepaid|my)(estub|htspace|mercy|nstrom|paycard|milestonecard|bpcreditcard|groundbiz|giftcardsite|pascoconnect|loweslife|balancenow|aarpmedicare|ccpay|cardstatement|cardstatus)\\.[a-z]{2,6}",
39+
Implementations.Pattern.FLAG_CASE_INSENSITIVE);
40+
}
41+
42+
@Benchmark
43+
public void caseInsensitiveSubMatch(Blackhole bh) {
44+
Implementations.Matcher matcher = binary ? pattern.matcher(bytes) : pattern.matcher(text);
45+
int count = 0;
46+
while (matcher.find()) {
47+
bh.consume(matcher.group());
48+
count++;
49+
}
50+
if (count != 0) {
51+
throw new AssertionError("Expected to not match anything");
52+
}
53+
}
54+
}

benchmarks/src/main/java/com/google/re2j/benchmark/BenchmarkSubMatch.java

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
import org.openjdk.jmh.annotations.State;
1515
import org.openjdk.jmh.infra.Blackhole;
1616

17-
import java.io.ByteArrayOutputStream;
18-
import java.io.IOException;
19-
import java.io.InputStream;
2017
import java.nio.charset.StandardCharsets;
2118
import java.util.concurrent.TimeUnit;
2219

@@ -30,7 +27,7 @@ public class BenchmarkSubMatch {
3027
@Param({"true", "false"})
3128
private boolean binary;
3229

33-
byte[] bytes = readFile("google-maps-contact-info.html");
30+
byte[] bytes = BenchmarkUtils.readResourceFile("google-maps-contact-info.html");
3431
private String html = new String(bytes, StandardCharsets.UTF_8);
3532

3633
private Implementations.Pattern pattern;
@@ -52,17 +49,4 @@ public void findPhoneNumbers(Blackhole bh) {
5249
throw new AssertionError("Expected to match one phone number.");
5350
}
5451
}
55-
56-
private static byte[] readFile(String name) {
57-
try (InputStream in = BenchmarkSubMatch.class.getClassLoader().getResourceAsStream(name);
58-
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
59-
int read;
60-
while ((read = in.read()) > -1) {
61-
out.write(read);
62-
}
63-
return out.toByteArray();
64-
} catch (IOException e) {
65-
throw new RuntimeException(e);
66-
}
67-
}
6852
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) 2022 The Go Authors. All rights reserved.
3+
*
4+
* Use of this source code is governed by a BSD-style
5+
* license that can be found in the LICENSE file.
6+
*/
7+
package com.google.re2j.benchmark;
8+
9+
import java.io.ByteArrayOutputStream;
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
13+
public class BenchmarkUtils {
14+
15+
// readResourceFile reads the contents of the Java resource file at the given path.
16+
public static byte[] readResourceFile(String name) {
17+
try (InputStream in = BenchmarkUtils.class.getClassLoader().getResourceAsStream(name);
18+
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
19+
int read;
20+
while ((read = in.read()) > -1) {
21+
out.write(read);
22+
}
23+
return out.toByteArray();
24+
} catch (IOException e) {
25+
throw new RuntimeException(e);
26+
}
27+
}
28+
29+
private BenchmarkUtils() {}
30+
}

benchmarks/src/main/java/com/google/re2j/benchmark/Implementations.java

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,20 @@ public String group() {
6767

6868
public abstract static class Pattern {
6969

70+
// FLAG_CASE_INSENSITIVE is an implementation-agnostic bitmask flag
71+
// indicating that a pattern should be case-insensitive.
72+
public static final int FLAG_CASE_INSENSITIVE = 1;
73+
7074
public static Pattern compile(Implementations impl, String pattern) {
75+
return compile(impl, pattern, 0);
76+
}
77+
78+
public static Pattern compile(Implementations impl, String pattern, int flags) {
7179
switch (impl) {
7280
case JDK:
73-
return new JdkPattern(pattern);
81+
return new JdkPattern(pattern, flags);
7482
case RE2J:
75-
return new Re2Pattern(pattern);
83+
return new Re2Pattern(pattern, flags);
7684
default:
7785
throw new AssertionError();
7886
}
@@ -88,8 +96,19 @@ public static class JdkPattern extends Pattern {
8896

8997
private final java.util.regex.Pattern pattern;
9098

91-
public JdkPattern(String pattern) {
92-
this.pattern = java.util.regex.Pattern.compile(pattern);
99+
public JdkPattern(String pattern, int flags) {
100+
int jdkPatternFlags = 0;
101+
102+
// For case-insensitive matching, explicitly enable both case-insensitive matching
103+
// and Unicode-aware case folding for this j.u.r.Pattern.
104+
// Merely enabling case-insensitive matching will cause the j.u.r.Pattern to assume
105+
// ASCII input and skip Unicode-aware case folding.
106+
if ((flags & FLAG_CASE_INSENSITIVE) > 0) {
107+
jdkPatternFlags |= java.util.regex.Pattern.CASE_INSENSITIVE;
108+
jdkPatternFlags |= java.util.regex.Pattern.UNICODE_CASE;
109+
}
110+
111+
this.pattern = java.util.regex.Pattern.compile(pattern, jdkPatternFlags);
93112
}
94113

95114
@Override
@@ -112,8 +131,12 @@ public static class Re2Pattern extends Pattern {
112131

113132
private final com.google.re2j.Pattern pattern;
114133

115-
public Re2Pattern(String pattern) {
116-
this.pattern = com.google.re2j.Pattern.compile(pattern);
134+
public Re2Pattern(String pattern, int flags) {
135+
int re2PatternFlags = 0;
136+
if ((flags & FLAG_CASE_INSENSITIVE) > 0) {
137+
re2PatternFlags |= com.google.re2j.Pattern.CASE_INSENSITIVE;
138+
}
139+
this.pattern = com.google.re2j.Pattern.compile(pattern, re2PatternFlags);
117140
}
118141

119142
@Override
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
Lórum ipse talán a körös, völő a legkevésbé aggodalkan. A szülényöt nem lehet kantnia, de meg lehet büdösödnie, tramót
2+
hevnie. Az egyik, hogy áptin fagyvához szélyeznie kell a struccot a szövedetséghez. A fagyva táns időközönként szemegi
3+
azokat a vinákat, akik nem pedzkezéltek hozzá a falásokhoz. Ez általában a neter külésén zagaznan, de nem minden tányságban).
4+
Az élhely helyesen kaponálog meg, ha talmasos ódogály van beállítva (klán a „kapé +1” bosztaganba szalkol). Ezt a fárát
5+
csak kord vinák talmazhatják fajtásba. Ha biztosan a talmasos ódogály van beállítva, akkor feltehetőleg azért nem gyentes,
6+
mivel reszet lehet a harás dián.
7+
8+
A szeleteken feli hullásokat azonban csependeznie kell, és nem zátkacs kezések dalan piszteteinek kőzésére hajganyoznia.
9+
Konlat szemés matódágot sóvadt ütődésök (busztikus és jedés ütődésök, fűző fogtat) haságában az egység kezésben fojtott
10+
szülésök vezők. Álám csalan vélvetek hiányában a külön lemérben egyén üdöngő reszletelés alapján. - a jelen polódás sofőn
11+
teli funciája szerint, amennyiben a latika vagy latikák nem gurítnak az itató istában, vagy gurítnak ugyan, de ott nincs
12+
hozzájuk rendelve sujas szalovagyás. Slem ha vannak csalan vélvetek, akkor a vira kezésben fojtott szülésök alapján,
13+
kivéve a vira konlat kezésben egyén busztikus, jedés, és fűző fogyást mazsánc ütődésöket, amelyeket a sofőn teli funcia
14+
„mető” karhelében egyén üdöngő reszleteléssel kell hebédnie. Amennyiben a fogás kezések, csalan vélvetek alapján bokrol,
15+
a csalan vélveteknek felinek kell lenniük szeletre is, azaz a meteleteknek udott módon fregzálniuk kell a szeletekre
16+
ciszti márkoldozásokat. A kanzásra szeres latikák és akangálok kart nyugos fogtatának emtője türkmés egy, a leviszok
17+
zokmás pasztránát lehetővé szikér reszleteléssel, vagy a kényszerű glág pipényesével [golygó (szocdes) glág fabag].
18+
19+
Lórum ipse számos máshol nehezen szülős bérzetet és akásos nészest is gőzik. A jénzés az oforsás szerikése, amely a nyiros
20+
cigorúságot marc érdelődi. - folyos helles kelídségként ez datásban mintegy neményező hárlanságot kokszoltak fel, ami a
21+
rományos datás hitatos csípő jénzéstől több, mint funsz gyalmatlan kadonossal dugol el. A folyos kelídségekből a kedésben
22+
halmas kadonos, ami még őrző tőcében a sali puffadt feles gaság által paradás csípőkből fodonított, nem érte el a cutbamás
23+
hárlanságot. Ebből a ságavas kelídség prázs hárlanságot parsápogt ki, a telő kelídség pedig gyesedte a hígság hárlanságot.
24+
A költség datást pirág levica még kült volt, hiszen az ez puffadt kelídségről a topis és izzadt kedet áriusa kedés első
25+
sebérőiben tetlegt meg. Így hetsze a toron ranyos jénzés őrző cserese, hiszen az árius hengje több göngyet esetén foszlékony
26+
hatlan, tehát a kelídséget prés falmatától lehet szennyeznie. Vadmányság a datás első senyvben függönyös szengeségként
27+
halmas spol hárlanság a szata gyalmatlan tízel oforsás csillatát parsápogta ki.
28+
29+
Szális érítmény, hogyan fognak kaporoznia zavadékony ortoroh, ha nem lesz tank. A manítáp sokkal tovább vásodik majd,
30+
mint a vánság halcán hatlan, de a manítáp nem kívós hullák dicsőültjének. Ez a tedalhanya szorosan szakodik a ségi bokarás
31+
lombácsához. A köző dicsőültök hákár lakoltja zsint fóka 0-3000-ig. A tank és a dózsa a heli nagramának egy oszlokáig
32+
menekelkedik. Szöszkes görzenlelése karágos, hogy a köző hozatok, az öngő, a konsátány és a tank ináda a tető salata iség
33+
szampós néződését tögeskezi. Ez a dózsa úgy ábol a mezőben, mint a heredás izzása, borsalja a fogatot, de nalanja a
34+
sültök által nácstagos talan iséget.
35+
36+
A sulás banúval pakarál, házásai batányos pariszt ségzőből pakarálnak, kacér házása a tonccal szalminális, fedése táns
37+
fajogány, a kukók fatlan kező ámos formányos - válkatos sembecserejek. A tösdön, a szildás fónin egy zátott gyülég, ill.
38+
egy zátott ruzgomatás, míg a szakony fónin latózok tékednek esztözre. A számortó stehésen 48-100 m2 pintohos jális gyülégök
39+
staságára van páns. A filevés folyása a szeges gyülégökhöz rendelve tékedik esztözre, így ezek a gyülégök kajósak.
40+
A törömnyi gyülég, latóz, jedés törmendésének hábája a brómok szerint vízlik: A gráta során bizony elég sok környe
41+
ügyködt, amelynek egy varását utólag el lehetett sodnia, más varása gyezőnek vitelt. Az éredő nem fúlódt tatlannak,
42+
bár a tendó ínyes csiszésekben nem elég fokos lévén, kötérbe se nyilajtott a saját satott gráta.
43+
44+
Tizmus a pacozások odásánál ösztésben pombiskálnak a kürtők és kernák vaglárájával kulla sodúk. Fegyítés a dária száromozása
45+
a kohé bókásos lyukáinak polos száromozása is lehet. Kezés a hozás művelt nyúlt, csites legyen ; porcsata ne lalmazjon
46+
túl friss szortéros, fojtos, illetve ketes tödőket. A folyék cserkéjét, a sodú melgőjét és fenyőjét az alamok szabadon
47+
vilizálhatják meg. Tagság herő: dévatás pacozás: 180 teli, monság pacozás: 110 teli, bota pacozás: 60 teli. A neslés
48+
egész fonílásán éresítő trigy keredő lendikes zetíl (folány cocilkozás fogás törzs rimázat páter falás böjtő mit léka
49+
fogás kérde), melynek nyúlása a lendikes juhuzmusok és vihos tördője, matált kalomot silkodál a bükkös skum számítárára.
50+
Szulyái lopszli és himő zális bűnök hatódtak fel a lipés prodása címlés kohéján, a fatos zsürtegek alapján:
51+
52+
Mintegy 1000 ha varlánz nyonkája a fogtatlan, és doncsok tíz hódik jáltos varlánz csajbánya a sosos hajda a suvatok fájékos
53+
mahostarára. Ezt csatizással, a balan mutához hasonlóan, a nírtek esztekes ítékének (pityi) fűzesében títos kétezdeznie.
54+
A szike közösen volna títos urálnia azoknak a viergéseknek, akik nem valiskoznak ezes suvattal, és azoknak, akiknek van
55+
köszkes csúságuk. Ezeket a csúságokat hagyakodhatnák a pityibe, amíg a csúsággal nem bűnösek többé-kevésbé a fecselő cinget
56+
„haljadnák” a repern ítékbe. Títos lenne varannia a mekvény más szélyiségeinek is ezen suvatokban való kosztát. Ez a
57+
fűzes egyes plékben sérő a szeresti fariásban. Elsősorban a csávas viergéseket kodná azáltal, hogy nem volna esztekes
58+
szára számukra, amikor közvetlenül csens után igen fáns, tomorcos fendőn kell mozdeztetniük a varlánzukat.
59+
60+
A nalással és fenestikkel, csürgényökkel, menségekkel és pedéssel fikarok turvajkok is a szolvas fara sovácsait filtezik
61+
maság elé. A trony pantás elsősorban a randékok hajtáján tiska baksias hígságokkal tivódoz. A kélenemény maságait hetes,
62+
szolvas, páragos besztként szabványítja meg a peség minden hajtáján. A dalkármány folt sedicse (koncák, pacsos pajlág,
63+
termőr és fehes karság) hatos hesemben is lizoláz a sajlékony kedéseknek. A kezős paporásoknál a dura miatt +3 járót
64+
kell üzesereznie. Venc tekerenek: parkingban és göntésben: ravara ; feheregésben és lenemetben: redség. Folya tekeren:
65+
a palatás gyezőhöz: letlem, a második gyezőhöz: zombon.
66+
67+
A „szepokra” arma egyike azoknak, amelyek rengeteg szíjas andoknak és aktának kednek menicsora. Ahogy a baga fina is több
68+
mint húsz armát rodik a „hó” vítésére, a „szepokra” dingólyája is számtalan ingebentent vicskohat. Hódhatik például fontúrt,
69+
visztet, irátot, cicibizmust, a dohajtolás előkedét, s a hariát lehetne még passzolnia. „Nincs olyan, hogy egy várlóval
70+
ne sondítna valamiféle hamlomós balova is. A lizajzált smény ügezek általában egy (vagy több) olyan bajkozás, akta vagy
71+
lojt emény őrlését hódják, amelyek valamilyen bilománynál fogva nem szárznak meg a dulláknak. A szalan bátkas szulások
72+
felől nézve a lizajzál gyakran hódja a hugyos sejtesek és varcárok pális figyelem szinomát is. A lizajzál patlabora a
73+
lizmus törös lizajzálának is olatot, zákult szfilvet téveng.
74+
75+
Molás a fárd alkep gurnájára kedelt tikuflus és tonkolt derej körmet. A meret pest füle talant varcokságában a tödés nem
76+
a nyolc érdeges morgos mihanás folójával, hanem a tizedik mihanás folójakor szemző tigével molkodik le. A hozott köztes
77+
folád varcokságában azonban - az árzatos bájos mafrucákhoz buzatos netogok átlagosan 15 sutója számára csesti szigást
78+
igatos cseredres zsingenségektől eltekintve - a hozott halkolások az érdeges hadék végén szélyellnek. Ez azt kuskolja,
79+
hogy a netogok zetőr bizmusa lasztérzó hozott halkolásban méredik vizsmát. Pontosabban a kilencedik és a tizedik mihanásban
80+
nem mérednek vizsmát olyan halkolásban, amely birázja őket arra, hogy hozott ártáson lődjék le a tigét. Éppen az ártás az,
81+
ami miatt a hozott köztes lönövéjéből a rátos varcokság jelenleg nem szelső, márpedig a rátos varcokság tagálását tekintve
82+
az ábítás nyagvató idségei vizetik módnia az alom és az ehhez képest ébrengő füle talant tamangásokból emező farlókat.
83+
Az eges ványos tödés más varányból, de szintén tatók papija lehet a malan netogok számára is.
84+
85+
Viszont az egész szájék fürgőjét reteli katatos sörös szobzó neméréből már évenes bizárlát kell láznia és gyújtnia kell
86+
ellene. Szóval a lehető évenes dugság arról szadoznia, hogy minden atyus evező külésökkel buzonál. Ez már csak azért is
87+
dugság, mert ha nem szántna az állott szinó, akkor sem buzonálna minden atyus evező külésökkel. A szordiumban és
88+
tulajdonképpen az imányban, mindig az nyúlékosnak vannak külései. Egyébként ez is az állott szinó lanságát kodja. A másik
89+
ami szintén tagadhatatlan, hogy a gyulucs összehasonlíthatatlanul többet kundozik bármely mezgő csokriumánál. A raca a
90+
bértő a hajtin érzéséből.
91+
92+
Rémes buzódást raccsol: „Egy basé az árlók, két basé az adások és ártályok, egy, vagy két basé a gomászok számára”.
93+
A gomász fukalai talányosak és világosan szélyeznek a szalkutyával és a tenélennel. Az észer gyatás maság nem lehet
94+
kevésbé hatlan a holás és selég regeztetében, mint a salan habitások: a meszmerek, amiket a mezerek hangjában előre kednek,
95+
gyorsan kettősek, ha titos jövék szüregetnek fel. Végül, az észer gyatás maság háromtól öt rincig tung béresben kítos,
96+
és meg is kell csepítnie, ellentétben azokkal a nem busztos szaftos elkesekkel, amelyek az adék makáját gyakran szegetik.
97+
Ha bárki úgy csalmasztná nincs tobajban, nem adathatja őrizetlenül a vistalkáját, nem szalhatja le a hinatát avval a
98+
biztos csonyával, hogy ott lesz amikor hatozik. A barák valahogy mindig is csempekeztek szaldagra és a költésökre.
99+
A hűsítő, hidekes, szort és feli költésökre is.

java/com/google/re2j/Inst.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,11 @@ boolean matchRune(int r) {
5050
// class.
5151
if (runes.length == 1) {
5252
int r0 = runes[0];
53-
if (r == r0) {
54-
return true;
55-
}
53+
5654
if ((arg & RE2.FOLD_CASE) != 0) {
57-
for (int r1 = Unicode.simpleFold(r0); r1 != r0; r1 = Unicode.simpleFold(r1)) {
58-
if (r == r1) {
59-
return true;
60-
}
61-
}
55+
return Unicode.equalsIgnoreCase(r, r0);
6256
}
63-
return false;
57+
return r == r0;
6458
}
6559

6660
// Peek at the first few pairs.

java/com/google/re2j/Unicode.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,40 @@ static int simpleFold(int r) {
122122
return Characters.toUpperCase(r);
123123
}
124124

125+
// equalsIgnoreCase performs case-insensitive equality comparison
126+
// on the given runes |r1| and |r2|, with special consideration
127+
// for the likely scenario where both runes are ASCII characters.
128+
// -1 is interpreted as the end-of-file mark.
129+
static boolean equalsIgnoreCase(int r1, int r2) {
130+
// Runes already match, or one of them is EOF
131+
if (r1 < 0 || r2 < 0 || r1 == r2) {
132+
return true;
133+
}
134+
135+
// Fast path for the common case where both runes are ASCII characters.
136+
// Coerces both runes to lowercase if applicable.
137+
if (r1 <= MAX_ASCII && r2 <= MAX_ASCII) {
138+
if ('A' <= r1 && r1 <= 'Z') {
139+
r1 |= 0x20;
140+
}
141+
142+
if ('A' <= r2 && r2 <= 'Z') {
143+
r2 |= 0x20;
144+
}
145+
146+
return r1 == r2;
147+
}
148+
149+
// Fall back to full Unicode case folding otherwise.
150+
// Invariant: r1 must be non-negative
151+
for (int r = Unicode.simpleFold(r1); r != r1; r = Unicode.simpleFold(r)) {
152+
if (r == r2) {
153+
return true;
154+
}
155+
}
156+
157+
return false;
158+
}
159+
125160
private Unicode() {} // uninstantiable
126161
}

0 commit comments

Comments
 (0)