Fast request routing using regular expressions
nikic.github.io
nikic.github.io
Not that I'm slagging off their efforts, in fact I plan to use FastRoute in one of my projects.
Of course, in practice it's not. PCRE is backtracking-based rather than automaton-based, so increasing the complexity of the regex does have an effect on runtime.
Maybe I should implement those two things.
Note: There is no whitespace between * and MARK, but if I remove it HN formats it as an italic string.
Given `(?<g1>Sat)urday|(?<g2>Sun)day`, and a successful match, either `$+{g1}` or `$+{g2}` will hold the relevant subpart.
Quickie demo script:
my $re = "(?<g1>Sat)urday|(?<g2>Sun)day";
foreach my $d (qw(Friday Sunday Thursday Saturday)) {
my $out = '<default>';
if ($d =~ /$re/) {
if ($+{g1}) { $out = 'Saturday' }
elsif ($+{g2}) { $out = 'Sunday' }
}
print "$d routes to $out\n";
}Perl 5.10 from 2007 supports named captures. (Google confirmed that.)
Edit: nikic in the parallel comment is correct, named captures are affected the same way in Perl's regexp engine with ?|. (Google perlre and search for ?|.)
There's a common misconception that regular expressions are slow, but in many cases they offer much better performance than a manual implementation with lots of strpos etc. In the end PCRE does the same thing, but in C and using a JIT compiler. Apart from very simple cases you don't have a chance to keep up with that using PHP code.
As lmm points out (I think), one can get a big advantage precisely by not requiring the regex code to know this. (Regexes are 'really' DFAs, and a DFA that makes it to its start state not just does not but can not know how it got there.) The more power one gives regexes, the less, well, regular they are, and the slower they become to match.
You'd have to compile the DFA differently, of course. A really simple version would just compile each alternation into its own DFA separately, and then chain them together with "try each in sequence". That's still a DFA, so it's not a matter of a different class of computations, though it's often slower by a constant factor (due to not being able to share any information between the alternations when matching them).
https://github.com/darius/sketchbook/blob/master/lex/dfa_nfa...
P.S. I might as well mention that I obviously meant to refer to a DFA reaching its end, rather than start, state.
http://stackoverflow.com/questions/3287860/what-is-a-tagged-...
~^(?|
xx(x)/user/([^/]+)/(\d+)
| x(xx)/user/(\d+)
| (xxx)/user/([^/]+)
)$~x
Then just prepend 'xxx' to the front of uri in the call to preg_match. Now the second entry in matches tells you which handler, so for example for uri = '/user/nikic/42' you pass 'xxx/user/nikic/42' and get back 'xxx/user/nikic/42', 'x', 'nikic', and '42' where that 'x' tells you it's the first handler.I'm not an expert in how php does REs but I imagine there may be a way to use preg_replace or similar to replace the first '/' in the uri with 'x', 'xx', or 'xxx' and still get a the groups returned for the matches for user and id say. This could save a potentially costly string creation.
For this kind of problem you need to use actual regular expressions, not Perl's bastardization of the concept. Or produce your own DFA - it's not as hard as it looks, and gives you a much better idea what's going on.
Even if you do use some very weird expression, the worst that can happen is that you hit the backtracking limit and a route fails to match because of that. There won't be any code spinning indefinitely.
the example given can determine the route required by everything following /user/ - in the case that the 7th character of the string is a digit then we have the last case - all that remains to distinguish the other two is to count the number of slashes - if you want to distinguish bad matches then validating the extracted values will be necessary too. of course this kind of logic fails to scale to the general case - but for real world optimisation i can imagine it being much more fruitful (possibly you could do a match like i described faster than the regex function will parse its own input string before doing any matching at all!)
there is the smell of a very classical problem here though - "guess optimising". optimisation should be driven by measurement - identify the slow part then optimise it. timings that show the performance of the solutions overall are interesting and prove the point - but what would be more interesting imo would be 'this was the hot spot (point at timing), therefore i was right (point at new timing)'.
No results.
The chunking seems like a missed opportunity. It still uses a loop, so it's still O(n) in the number of routes. That is, 200 routes will take twice as long. 1000 routes will take 10 times as long. But maybe 100 routes is all we ever see in reality?
Regardless, it's a small leap from that to using a tree. I would like to have seen timings for that, and for larger numbers of routes.
Building a trie from a list of strings is trivial. Building one from a list of regexes is not. But you don't need the complexity of inspecting or interpreting the regexes.
With a regular binary tree, you'll need to compile multiple regexes. The root is a regex with all urls in only two groups: (a|b|c|d)|(e|f|g|h). If the first group is non-empty, then you move to the next node: (a|b)|(c|d). If the second group is non-empty, then you try (c)|(d). If the first group of that is non-empty, then the url was c. That should be O(k * log n) where k is the length of the url and n in the number of urls.
The grouping method requires 10 regex matches for 100 routes. 2^10 == 1024, so the tree method can do 10 times more routes in about the same amount of work. A million routes with a tree is only twice as hard as a thousand, or twice as hard as a hundred with chunking. A million routes with chunking in 10,000 times harder than a hundred.
On the other hand, the tree requires O(n * log n) memory, where n chunks of constant size requires only O(n) memory. Perhaps that's significant.
edit: misplaced asterisks triggered italics.
edit2: Actually, you don't even need to add or check groups. At each node you only need to combine half the regexes into one with no outer group. For example, the top node regex from above could be just a|b|c|d. If it matches, go to the left node (which is just a|b). If it doesn't match, go to the right node (which is just e|f). As an edge case, the right-most tip node will all need to have a regex to distinguish between it and non-matching urls. This also makes it easier to dynamically add routes without recompiling the whole tree. Just add the route to the right side of each node, which requires no work until you get to the rightmost tip. Occasionally rebalance. Is dynamically modifying the routes something that people do?
(((a)|(b))|((c)|(d))) | (((e)|(f))|((g)|(h)))
The number of groups will be O(n * log n). So 10-20 times the number of urls at most. It's certainly not quadratic.
The groups will in depth-first order. It's slightly complicated by the fact that the regexes contain capturing groups themselves, but you can precompute an offset table that accounts for them.
Or give each tree-group a name:
(?<0>(?<00>(?<000>a)|(?<001>b))|(?<01>(?<010>c)|(?<011>d))) | (?<1>(?<10>(?<100>e)|(?<101>f))|(?<11>(?<110>g)|(?<111>h)))
If you use names like that, walking the tree is easy. Just concatenate "0" or "1" to the current node node to find the child nodes. Also, the final matching group name is a binary number corresponding to matching route's index into the list of routes. For example, url "e" has the name "100". That's a binary 4, which is url "e"'s position in the list.
(I don't know how it would stand up against Journey, the Rails 4 router business. I imagine it's only gotten faster so there may be next to no gain)