Skip to content

Commit

Permalink
Add fast path for ASCII case folding
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
mszabo-wikia authored and sjamesr committed Jul 10, 2022
1 parent 3e685d9 commit dc7d6e5
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2022 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.
*/
package com.google.re2j.benchmark;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

// BenchmarkCaseInsensitiveSubmatch tests the performance of case-insensitive matching
// by testing a mostly ASCII regex pattern versus a moderately large text containing both
// ASCII and Unicode characters.
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class BenchmarkCaseInsensitiveSubmatch {
@Param({"JDK", "RE2J"})
private Implementations impl;

@Param({"true", "false"})
private boolean binary;

private final byte[] bytes = BenchmarkUtils.readResourceFile("unicode-sample-text.txt");

private final String text = new String(bytes, StandardCharsets.UTF_8);

private Implementations.Pattern pattern;

@Setup
public void setup() {
pattern =
Implementations.Pattern.compile(
impl,
"(prepaid|my)(estub|htspace|mercy|nstrom|paycard|milestonecard|bpcreditcard|groundbiz|giftcardsite|pascoconnect|loweslife|balancenow|aarpmedicare|ccpay|cardstatement|cardstatus)\\.[a-z]{2,6}",
Implementations.Pattern.FLAG_CASE_INSENSITIVE);
}

@Benchmark
public void caseInsensitiveSubMatch(Blackhole bh) {
Implementations.Matcher matcher = binary ? pattern.matcher(bytes) : pattern.matcher(text);
int count = 0;
while (matcher.find()) {
bh.consume(matcher.group());
count++;
}
if (count != 0) {
throw new AssertionError("Expected to not match anything");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

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

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

private Implementations.Pattern pattern;
Expand All @@ -52,17 +49,4 @@ public void findPhoneNumbers(Blackhole bh) {
throw new AssertionError("Expected to match one phone number.");
}
}

private static byte[] readFile(String name) {
try (InputStream in = BenchmarkSubMatch.class.getClassLoader().getResourceAsStream(name);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
int read;
while ((read = in.read()) > -1) {
out.write(read);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 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.
*/
package com.google.re2j.benchmark;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class BenchmarkUtils {

// readResourceFile reads the contents of the Java resource file at the given path.
public static byte[] readResourceFile(String name) {
try (InputStream in = BenchmarkUtils.class.getClassLoader().getResourceAsStream(name);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
int read;
while ((read = in.read()) > -1) {
out.write(read);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private BenchmarkUtils() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,20 @@ public String group() {

public abstract static class Pattern {

// FLAG_CASE_INSENSITIVE is an implementation-agnostic bitmask flag
// indicating that a pattern should be case-insensitive.
public static final int FLAG_CASE_INSENSITIVE = 1;

public static Pattern compile(Implementations impl, String pattern) {
return compile(impl, pattern, 0);
}

public static Pattern compile(Implementations impl, String pattern, int flags) {
switch (impl) {
case JDK:
return new JdkPattern(pattern);
return new JdkPattern(pattern, flags);
case RE2J:
return new Re2Pattern(pattern);
return new Re2Pattern(pattern, flags);
default:
throw new AssertionError();
}
Expand All @@ -88,8 +96,19 @@ public static class JdkPattern extends Pattern {

private final java.util.regex.Pattern pattern;

public JdkPattern(String pattern) {
this.pattern = java.util.regex.Pattern.compile(pattern);
public JdkPattern(String pattern, int flags) {
int jdkPatternFlags = 0;

// For case-insensitive matching, explicitly enable both case-insensitive matching
// and Unicode-aware case folding for this j.u.r.Pattern.
// Merely enabling case-insensitive matching will cause the j.u.r.Pattern to assume
// ASCII input and skip Unicode-aware case folding.
if ((flags & FLAG_CASE_INSENSITIVE) > 0) {
jdkPatternFlags |= java.util.regex.Pattern.CASE_INSENSITIVE;
jdkPatternFlags |= java.util.regex.Pattern.UNICODE_CASE;
}

this.pattern = java.util.regex.Pattern.compile(pattern, jdkPatternFlags);
}

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

private final com.google.re2j.Pattern pattern;

public Re2Pattern(String pattern) {
this.pattern = com.google.re2j.Pattern.compile(pattern);
public Re2Pattern(String pattern, int flags) {
int re2PatternFlags = 0;
if ((flags & FLAG_CASE_INSENSITIVE) > 0) {
re2PatternFlags |= com.google.re2j.Pattern.CASE_INSENSITIVE;
}
this.pattern = com.google.re2j.Pattern.compile(pattern, re2PatternFlags);
}

@Override
Expand Down
99 changes: 99 additions & 0 deletions benchmarks/src/main/resources/unicode-sample-text.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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
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
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).
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
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,
mivel reszet lehet a harás dián.

A szeleteken feli hullásokat azonban csependeznie kell, és nem zátkacs kezések dalan piszteteinek kőzésére hajganyoznia.
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
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
teli funciája szerint, amennyiben a latika vagy latikák nem gurítnak az itató istában, vagy gurítnak ugyan, de ott nincs
hozzájuk rendelve sujas szalovagyás. Slem ha vannak csalan vélvetek, akkor a vira kezésben fojtott szülésök alapján,
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
„mető” karhelében egyén üdöngő reszleteléssel kell hebédnie. Amennyiben a fogás kezések, csalan vélvetek alapján bokrol,
a csalan vélveteknek felinek kell lenniük szeletre is, azaz a meteleteknek udott módon fregzálniuk kell a szeletekre
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
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].

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
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
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
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
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.
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ő
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
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
halmas spol hárlanság a szata gyalmatlan tízel oforsás csillatát parsápogta ki.

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,
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
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
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
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
sültök által nácstagos talan iséget.

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
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.
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
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.
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
ü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,
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.

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
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
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
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
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
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.
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:

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
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.
A szike közösen volna títos urálnia azoknak a viergéseknek, akik nem valiskoznak ezes suvattal, és azoknak, akiknek van
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
„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
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
szára számukra, amikor közvetlenül csens után igen fáns, tomorcos fendőn kell mozdeztetniük a varlánzukat.

A nalással és fenestikkel, csürgényökkel, menségekkel és pedéssel fikarok turvajkok is a szolvas fara sovácsait filtezik
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,
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,
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
kell üzesereznie. Venc tekerenek: parkingban és göntésben: ravara ; feheregésben és lenemetben: redség. Folya tekeren:
a palatás gyezőhöz: letlem, a második gyezőhöz: zombon.

A „szepokra” arma egyike azoknak, amelyek rengeteg szíjas andoknak és aktának kednek menicsora. Ahogy a baga fina is több
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,
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
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
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
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
lizmus törös lizajzálának is olatot, zákult szfilvet téveng.

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
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
folád varcokságában azonban - az árzatos bájos mafrucákhoz buzatos netogok átlagosan 15 sutója számára csesti szigást
igatos cseredres zsingenségektől eltekintve - a hozott halkolások az érdeges hadék végén szélyellnek. Ez azt kuskolja,
hogy a netogok zetőr bizmusa lasztérzó hozott halkolásban méredik vizsmát. Pontosabban a kilencedik és a tizedik mihanásban
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,
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
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.
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.

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
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
dugság, mert ha nem szántna az állott szinó, akkor sem buzonálna minden atyus evező külésökkel. A szordiumban és
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
ami szintén tagadhatatlan, hogy a gyulucs összehasonlíthatatlanul többet kundozik bármely mezgő csokriumánál. A raca a
bértő a hajtin érzéséből.

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”.
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
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,
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,
és meg is kell csepítnie, ellentétben azokkal a nem busztos szaftos elkesekkel, amelyek az adék makáját gyakran szegetik.
Ha bárki úgy csalmasztná nincs tobajban, nem adathatja őrizetlenül a vistalkáját, nem szalhatja le a hinatát avval a
biztos csonyával, hogy ott lesz amikor hatozik. A barák valahogy mindig is csempekeztek szaldagra és a költésökre.
A hűsítő, hidekes, szort és feli költésökre is.
12 changes: 3 additions & 9 deletions java/com/google/re2j/Inst.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,11 @@ boolean matchRune(int r) {
// class.
if (runes.length == 1) {
int r0 = runes[0];
if (r == r0) {
return true;
}

if ((arg & RE2.FOLD_CASE) != 0) {
for (int r1 = Unicode.simpleFold(r0); r1 != r0; r1 = Unicode.simpleFold(r1)) {
if (r == r1) {
return true;
}
}
return Unicode.equalsIgnoreCase(r, r0);
}
return false;
return r == r0;
}

// Peek at the first few pairs.
Expand Down
35 changes: 35 additions & 0 deletions java/com/google/re2j/Unicode.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,40 @@ static int simpleFold(int r) {
return Characters.toUpperCase(r);
}

// equalsIgnoreCase performs case-insensitive equality comparison
// on the given runes |r1| and |r2|, with special consideration
// for the likely scenario where both runes are ASCII characters.
// -1 is interpreted as the end-of-file mark.
static boolean equalsIgnoreCase(int r1, int r2) {
// Runes already match, or one of them is EOF
if (r1 < 0 || r2 < 0 || r1 == r2) {
return true;
}

// Fast path for the common case where both runes are ASCII characters.
// Coerces both runes to lowercase if applicable.
if (r1 <= MAX_ASCII && r2 <= MAX_ASCII) {
if ('A' <= r1 && r1 <= 'Z') {
r1 |= 0x20;
}

if ('A' <= r2 && r2 <= 'Z') {
r2 |= 0x20;
}

return r1 == r2;
}

// Fall back to full Unicode case folding otherwise.
// Invariant: r1 must be non-negative
for (int r = Unicode.simpleFold(r1); r != r1; r = Unicode.simpleFold(r)) {
if (r == r2) {
return true;
}
}

return false;
}

private Unicode() {} // uninstantiable
}
Loading

0 comments on commit dc7d6e5

Please sign in to comment.