Categories
Posts in this category
- Current State of Exceptions in Rakudo and Perl 6
- Meet DBIish, a Perl 6 Database Interface
- doc.perl6.org and p6doc
- Exceptions Grant Report for May 2012
- Exceptions Grant Report -- Final update
- Perl 6 Hackathon in Oslo: Be Prepared!
- Localization for Exception Messages
- News in the Rakudo 2012.05 release
- News in the Rakudo 2012.06 release
- Perl 6 Hackathon in Oslo: Report From The First Day
- Perl 6 Hackathon in Oslo: Report From The Second Day
- Quo Vadis Perl?
- Rakudo Hack: Dynamic Export Lists
- SQLite support for DBIish
- Stop The Rewrites!
- Upcoming Perl 6 Hackathon in Oslo, Norway
- A small regex optimization for NQP and Rakudo
- Pattern Matching and Unpacking
- Rakudo's Abstract Syntax Tree
- The REPL trick
- First day at YAPC::Europe 2013 in Kiev
- YAPC Europe 2013 Day 2
- YAPC Europe 2013 Day 3
- A new Perl 6 community server - call for funding
- New Perl 6 community server now live, accepting signups
- A new Perl 6 community server - update
- All Perl 6 modules in a box
- doc.perl6.org: some stats, future directions
- Profiling Perl 6 code on IRC
- Why is it hard to write a compiler for Perl 6?
- Writing docs helps you take the user's perspective
- Perl 6 Advent Calendar 2016 -- Call for Authors
- Perl 6 By Example: Running Rakudo
- Perl 6 By Example: Formatting a Sudoku Puzzle
- Perl 6 By Example: Testing the Say Function
- Perl 6 By Example: Testing the Timestamp Converter
- Perl 6 By Example: Datetime Conversion for the Command Line
- What is Perl 6?
- Perl 6 By Example, Another Perl 6 Book
- Perl 6 By Example: Silent Cron, a Cron Wrapper
- Perl 6 By Example: Testing Silent Cron
- Perl 6 By Example: Stateful Silent Cron
- Perl 6 By Example: Perl 6 Review
- Perl 6 By Example: Parsing INI files
- Perl 6 By Example: Improved INI Parsing with Grammars
- Perl 6 By Example: Generating Good Parse Errors from a Parser
- Perl 6 By Example: A File and Directory Usage Graph
- Perl 6 By Example: Functional Refactorings for Directory Visualization Code
- Perl 6 By Example: A Unicode Search Tool
- What's a Variable, Exactly?
- Perl 6 By Example: Plotting using Matplotlib and Inline::Python
- Perl 6 By Example: Stacked Plots with Matplotlib
- Perl 6 By Example: Idiomatic Use of Inline::Python
- Perl 6 By Example: Now "Perl 6 Fundamentals"
- Perl 6 Books Landscape in June 2017
- Living on the (b)leading edge
- The Loss of Name and Orientation
- Perl 6 Fundamentals Now Available for Purchase
- My Ten Years of Perl 6
- Perl 6 Coding Contest 2019: Seeking Task Makers
- A shiny perl6.org site
- Creating an entry point for newcomers
- An offer for software developers: free IRC logging
- Sprixel, a 6 compiler powered by JavaScript
- Announcing try.rakudo.org, an interactive Perl 6 shell in your browser
- Another perl6.org iteration
- Blackjack and Perl 6
- Why I commit Crud to the Perl 6 Test Suite
- This Week's Contribution to Perl 6 Week 5: Implement Str.trans
- This Week's Contribution to Perl 6
- This Week's Contribution to Perl 6 Week 8: Implement $*ARGFILES for Rakudo
- This Week's Contribution to Perl 6 Week 6: Improve Book markup
- This Week's Contribution to Perl 6 Week 2: Fix up a test
- This Week's Contribution to Perl 6 Week 9: Implement Hash.pick for Rakudo
- This Week's Contribution to Perl 6 Week 11: Improve an error message for Hyper Operators
- This Week's Contribution to Perl 6 - Lottery Intermission
- This Week's Contribution to Perl 6 Week 3: Write supporting code for the MAIN sub
- This Week's Contribution to Perl 6 Week 1: A website for proto
- This Week's Contribution to Perl 6 Week 4: Implement :samecase for .subst
- This Week's Contribution to Perl 6 Week 10: Implement samespace for Rakudo
- This Week's Contribution to Perl 6 Week 7: Implement try.rakudo.org
- What is the "Cool" class in Perl 6?
- Report from the Perl 6 Hackathon in Copenhagen
- Custom operators in Rakudo
- A Perl 6 Date Module
- Defined Behaviour with Undefined Values
- Dissecting the "Starry obfu"
- The case for distributed version control systems
- Perl 6: Failing Softly with Unthrown Exceptions
- Perl 6 Compiler Feature Matrix
- The first Perl 6 module on CPAN
- A Foray into Perl 5 land
- Gabor: Keep going
- First Grant Report: Structured Error Messages
- Second Grant Report: Structured Error Messages
- Third Grant Report: Structured Error Messages
- Fourth Grant Report: Structured Error Messages
- Google Summer of Code Mentor Recap
- How core is core?
- How fast is Rakudo's "nom" branch?
- Building a Huffman Tree With Rakudo
- Immutable Sigils and Context
- Is Perl 6 really Perl?
- Mini-Challenge: Write Your Prisoner's Dilemma Strategy
- List.classify
- Longest Palindrome by Regex
- Perl 6: Lost in Wonderland
- Lots of momentum in the Perl 6 community
- Monetize Perl 6?
- Musings on Rakudo's spectest chart
- My first executable from Perl 6
- My first YAPC - YAPC::EU 2010 in Pisa
- Trying to implement new operators - failed
- Programming Languages Are Not Zero Sum
- Perl 6 notes from February 2011
- Notes from the YAPC::EU 2010 Rakudo hackathon
- Let's build an object
- Perl 6 is optimized for fun
- How to get a parse tree for a Perl 6 Program
- Pascal's Triangle in Perl 6
- Perl 6 in 2009
- Perl 6 in 2010
- Perl 6 in 2011 - A Retrospection
- Perl 6 ticket life cycle
- The Perl Survey and Perl 6
- The Perl 6 Advent Calendar
- Perl 6 Questions on Perlmonks
- Physical modeling with Math::Model and Perl 6
- How to Plot a Segment of a Circle with SVG
- Results from the Prisoner's Dilemma Challenge
- Protected Attributes Make No Sense
- Publicity for Perl 6
- PVC - Perl 6 Vocabulary Coach
- Fixing Rakudo Memory Leaks
- Rakudo architectural overview
- Rakudo Rocks
- Rakudo "star" announced
- My personal "I want a PONIE" wish list for Rakudo Star
- Rakudo's rough edges
- Rats and other pets
- The Real World Strikes Back - or why you shouldn't forbid stuff just because you think it's wrong
- Releasing Rakudo made easy
- Set Phasers to Stun!
- Starry Perl 6 obfu
- Recent Perl 6 Developments August 2008
- The State of Regex Modifiers in Rakudo
- Strings and Buffers
- Subroutines vs. Methods - Differences and Commonalities
- A SVG plotting adventure
- A Syntax Highlighter for Perl 6
- Test Suite Reorganization: How to move tests
- The Happiness of Design Convergence
- Thoughts on masak's Perl 6 Coding Contest
- The Three-Fold Function of the Smart Match Operator
- Perl 6 Tidings from September and October 2008
- Perl 6 Tidings for November 2008
- Perl 6 Tidings from December 2008
- Perl 6 Tidings from January 2009
- Perl 6 Tidings from February 2009
- Perl 6 Tidings from March 2009
- Perl 6 Tidings from April 2009
- Perl 6 Tidings from May 2009
- Perl 6 Tidings from May 2009 (second iteration)
- Perl 6 Tidings from June 2009
- Perl 6 Tidings from August 2009
- Perl 6 Tidings from October 2009
- Timeline for a syntax change in Perl 6
- Visualizing match trees
- Want to write shiny SVG graphics with Perl 6? Port Scruffy!
- We write a Perl 6 book for you
- When we reach 100% we did something wrong
- Where Rakudo Lives Now
- Why Rakudo needs NQP
- Why was the Perl 6 Advent Calendar such a Success?
- What you can write in Perl 6 today
- Why you don't need the Y combinator in Perl 6
- You are good enough!
Sat, 19 Oct 2013
A small regex optimization for NQP and Rakudo
Permanent link
Recently I read the course material of the Rakudo and NQP Internals Workshop, and had an idea for a small optimization for the regex engine. Yesterday night I implemented it, and I'd like to walk you through the process.
As a bit of background, the regex engine that Rakudo uses is actually implemented in NQP, and used by NQP too. The code I am about to discuss all lives in the NQP repository, but Rakudo profits from it too.
In addition one should note that the regex engine is mostly used for parsing grammar, a process which involves nearly no scanning. Scanning is the process where the regex engine first tries to match the regex at the start of the string, and if it fails there, moves to the second character in the string, tries again etc. until it succeeds.
But regexes that users write often involve scanning, and so my idea was to
speed up regexes that scan, and where the first thing in the regex is a
literal. In this case it makes sense to find possible start positions with a
fast string search algorithm, for example the
Boyer-Moore
algorithm. The virtual machine backends for NQP already implement that as
the index
opcode, which can be invoked as start = index
haystack, needle, startpos
, where the string haystack
is searched for the substring needle
, starting from position
startpos
.
From reading the course material I knew I had to search for a regex type
called scan
, so that's what I did:
$ git grep --word scan 3rdparty/libtommath/bn_error.c: /* scan the lookup table for the given message 3rdparty/libtommath/bn_mp_cnt_lsb.c: /* scan lower digits until non-zero */ 3rdparty/libtommath/bn_mp_cnt_lsb.c: /* now scan this digit until a 1 is found 3rdparty/libtommath/bn_mp_prime_next_prime.c: /* scan upwards 3rdparty/libtommath/changes.txt: -- Started the Depends framework, wrote d src/QRegex/P5Regex/Actions.nqp: QAST::Regex.new( :rxtype<sca src/QRegex/P6Regex/Actions.nqp: QAST::Regex.new( :rxtype<sca src/vm/jvm/QAST/Compiler.nqp: method scan($node) { src/vm/moar/QAST/QASTRegexCompilerMAST.nqp: method scan($node) { Binary file src/vm/moar/stage0/NQPP6QRegexMoar.moarvm matches Binary file src/vm/moar/stage0/QASTMoar.moarvm matches src/vm/parrot/QAST/Compiler.nqp: method scan($node) { src/vm/parrot/stage0/P6QRegex-s0.pir: $P5025 = $P5024."new"("scan" :named("rx src/vm/parrot/stage0/QAST-s0.pir:.sub "scan" :subid("cuid_135_1381944260.6802") src/vm/parrot/stage0/QAST-s0.pir: push $P5004, "scan"
The binary files and .pir files are generated code included just for
bootstrapping, and not interesting for us. The files in
3rdparty/libtommath
are there for bigint handling, thus not
interesting for us either. The rest are good matches:
src/QRegex/P6Regex/Actions.nqp
is responsible for compiling Perl
6 regexes to an abstract syntax tree (AST), and
src/vm/parrot/QAST/Compiler.nqp
compiles that AST down to PIR,
the assembly language that the Parrot Virtual Machine understands.
So, looking at src/QRegex/P6Regex/Actions.nqp
the place that
mentions scan
looked like this:
$block<orig_qast> := $qast; $qast := QAST::Regex.new( :rxtype<concat>, QAST::Regex.new( :rxtype<scan> ), $qast, ($anon ?? QAST::Regex.new( :rxtype<pass> ) !! (nqp::substr(%*RX<name>, 0, 12) ne '!!LATENAME!!' ?? QAST::Regex.new( :rxtype<pass>, :name(%*RX<name>) ) !! QAST::Regex.new( :rxtype<pass>, QAST::Var.new( :name(nqp::substr(%*RX<name>, 12)), :scope('lexical') ) ) )));
So to make the regex scan, the AST (in $qast
) is wrapped in
QAST::Regex.new(:rxtype<concat>,QAST::Regex.new(
:rxtype<scan> ), $qast, ...)
, plus some stuff I don't care
about.
To make the optimization work, the scan
node needs to know
what to scan for, if the first thing in the regex is indeed a constant string,
aka literal. If it is, $qast
is either directly of rxtype
literal
, or a concat
node where the first child is a
literal. As a patch, it looks like this:
--- a/src/QRegex/P6Regex/Actions.nqp +++ b/src/QRegex/P6Regex/Actions.nqp @@ -667,9 +667,21 @@ class QRegex::P6Regex::Actions is HLL::Actions { self.store_regex_nfa($code_obj, $block, QRegex::NFA.new.addnode($qast)) self.alt_nfas($code_obj, $block, $qast); + my $scan := QAST::Regex.new( :rxtype<scan> ); + { + my $q := $qast; + if $q.rxtype eq 'concat' && $q[0] { + $q := $q[0] + } + if $q.rxtype eq 'literal' { + nqp::push($scan, $q[0]); + $scan.subtype($q.subtype); + } + } + $block<orig_qast> := $qast; $qast := QAST::Regex.new( :rxtype<concat>, - QAST::Regex.new( :rxtype<scan> ), + $scan, $qast,
Since concat
nodes have always been empty so far, the code
generators don't look at their child nodes, and adding one with
nqp::push($scan, $q[0]);
won't break anything on backends that
don't support this optimization yet (which after just this patch were all of
them). Running make test
confirmed that.
My original patch did not contain the line
$scan.subtype($q.subtype);
, and later on some unit tests started
to fail, because regex matches can be case insensitive, but the
index
op works only case sensitive. For case insensitive matches,
the $q.subtype
of the literal regex node would be
ignorecase
, so that information needs to be carried on to the
code generation backend.
Once that part was in place, and some debug nqp::say()
statements confirmed that it indeed worked, it was time to look at the code
generation. For the parrot backend, it looked like this:
method scan($node) { my $ops := self.post_new('Ops', :result(%*REG<cur>)); my $prefix := self.unique('rxscan'); my $looplabel := self.post_new('Label', :name($prefix ~ '_loop')); my $scanlabel := self.post_new('Label', :name($prefix ~ '_scan')); my $donelabel := self.post_new('Label', :name($prefix ~ '_done')); $ops.push_pirop('repr_get_attr_int', '$I11', 'self', %*REG<curclass>, '"$!from"'); $ops.push_pirop('ne', '$I11', -1, $donelabel); $ops.push_pirop('goto', $scanlabel); $ops.push($looplabel); $ops.push_pirop('inc', %*REG<pos>); $ops.push_pirop('gt', %*REG<pos>, %*REG<eos>, %*REG<fail>); $ops.push_pirop('repr_bind_attr_int', %*REG<cur>, %*REG<curclass>, '"$!from"', %*REG<pos>); $ops.push($scanlabel); self.regex_mark($ops, $looplabel, %*REG<pos>, 0); $ops.push($donelabel); $ops; }
While a bit intimidating at first, staring at it for a while quickly made
clear what kind of code it emits. First three labels are generated, to which
the code can jump with goto $label
: One as a jump target for the
loop that increments the cursor position ($looplabel
), one for doing the regex match at
that position ($scanlabel
), and $donelabel
for
jumping to when the whole thing has finished.
Inside the loop there is an increment (inc) of the register the holds
the current position (%*REG<pos>
), that position is
compared to the end-of-string position (%*REG<eos>
), and if
is larger, the cursor is marked as failed.
So the idea is to advance the position by one, and then instead of doing
the regex match immediately, call the index
op to find the next
position where the regex might succeed:
--- a/src/vm/parrot/QAST/Compiler.nqp +++ b/src/vm/parrot/QAST/Compiler.nqp @@ -1564,7 +1564,13 @@ class QAST::Compiler is HLL::Compiler { $ops.push_pirop('goto', $scanlabel); $ops.push($looplabel); $ops.push_pirop('inc', %*REG<pos>); - $ops.push_pirop('gt', %*REG<pos>, %*REG<eos>, %*REG<fail>); + if nqp::elems($node.list) && $node.subtype ne 'ignorecase' { + $ops.push_pirop('index', %*REG<pos>, %*REG<tgt>, self.rxescape($node[0]), %*REG<pos>); + $ops.push_pirop('eq', %*REG<pos>, -1, %*REG<fail>); + } + else { + $ops.push_pirop('gt', %*REG<pos>, %*REG<eos>, %*REG<fail>); + } $ops.push_pirop('repr_bind_attr_int', %*REG<cur>, %*REG<curclass>, '"$!from"', %*REG<pos>); $ops.push($scanlabel); self.regex_mark($ops, $looplabel, %*REG<pos>, 0);
The index
op returns -1 on failure, so the condition for a
cursor fail are slightly different than before.
And as mentioned earlier, the optimization can only be safely done for matches that don't ignore case. Maybe with some additional effort that could be remedied, but it's not as simple as case-folding the target string, because some case folding operations can change the string length (for example ß becomes SS while uppercasing).
After successfully testing the patch, I came up with a small, artifical benchmark designed to show a difference in performance for this particular case. And indeed, it sped it up from 647 ± 28 µs to 161 ± 18 µs, which is roughly a factor of four.
You can see the whole thing as two commits on github.
What remains to do is implementing the same optimization on the JVM and MoarVM
backends, and of course other optimizations. For example the Perl 5 regex
engine keeps track of minimal and maximal string lengths for each subregex,
and can anchor a regex like /a?b?longliteral/
to 0..2 characters
before a match of longliteral
, and generally use that meta
information to fail faster.
But for now I am mostly encouraged that doing a worthwhile optimization was possible in a single evening without any black magic, or too intimate knowledge of the code generation.
Update: the code generation for MoarVM now
also uses the index op. The logic is the same as for the parrot backend,
the only difference is that the literal needs to be loaded into a register
(whose name fresh_s
returns) before index_s
can use
it.