Skip to content

Commit

Permalink
[Routing] improve matching performance by using possesive quantifiers…
Browse files Browse the repository at this point in the history
… when possible (closes #5471)

My benchmarks showed a performance improvement of 20% when matching routes that make use of possesive quantifiers because it prevents backtracking when it's not needed
  • Loading branch information
Tobion authored and fabpot committed Oct 3, 2012
1 parent a3147e9 commit 4eee88f
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 69 deletions.
8 changes: 8 additions & 0 deletions src/Symfony/Component/Routing/RouteCompiler.php
Expand Up @@ -79,6 +79,14 @@ public function compile(Route $route)
// part of {_format} when generating the URL, e.g. _format = 'mobile.html'.
$nextSeparator = $this->findNextSeparator($followingPattern);
$regexp = sprintf('[^/%s]+', '/' !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : '');
if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) {

This comment has been minimized.

Copy link
@staabm

staabm Oct 8, 2012

Contributor

dont know how hot this path is, but maybe doing the comparison the other way arround, would trigger the most expensive operation, the preg_match(), just when really required...

if ('' === $followingPattern || '' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) {

I also stripped one pair of parentesis, which were obsolete

This comment has been minimized.

Copy link
@Tobion

Tobion Oct 8, 2012

Author Member

Reversing the order has no effect here at all because preg_match cannot be called when following pattern is empty anyway.

This comment has been minimized.

Copy link
@staabm

staabm Oct 8, 2012

Contributor

Hmm the origin code executes the regex-function but the quoted code in my previous comment does not. So it saves the preg-match call, while the origin code everytime executes the preg-match, no matter if $followingPattern is empty or not,...

This comment has been minimized.

Copy link
@Tobion

Tobion Oct 8, 2012

Author Member

No. Please think about it. In my code:
'' !== $nextSeparator -> preg_match
'' === $nextSeparator -> no preg_match
In your code when '' === $followingPattern it means '' === $nextSeparator is also true. So you safe nothing.

// When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive
// quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns.
// Given the above example, there is no point in backtracking into {page} (that forbids the dot) when a dot must follow
// after it. This optimization cannot be applied when the next char is no real separator or when the next variable is
// directly adjacent, e.g. '/{x}{y}'.
$regexp .= '+';
}
}

$tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName);
Expand Down
Expand Up @@ -7,21 +7,21 @@ RewriteCond %{REQUEST_URI} ^/foo/(baz|symfony)$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:foo,E=_ROUTING_bar:%1,E=_ROUTING_DEFAULTS_def:test]

# foobar
RewriteCond %{REQUEST_URI} ^/foo(?:/([^/]+))?$
RewriteCond %{REQUEST_URI} ^/foo(?:/([^/]++))?$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:foobar,E=_ROUTING_bar:%1,E=_ROUTING_DEFAULTS_bar:toto]

# bar
RewriteCond %{REQUEST_URI} ^/bar/([^/]+)$
RewriteCond %{REQUEST_URI} ^/bar/([^/]++)$
RewriteCond %{REQUEST_METHOD} !^(GET|HEAD)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING__allow_GET:1,E=_ROUTING__allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/bar/([^/]+)$
RewriteCond %{REQUEST_URI} ^/bar/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:bar,E=_ROUTING_foo:%1]

# baragain
RewriteCond %{REQUEST_URI} ^/baragain/([^/]+)$
RewriteCond %{REQUEST_URI} ^/baragain/([^/]++)$
RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING__allow_GET:1,E=_ROUTING__allow_POST:1,E=_ROUTING__allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/baragain/([^/]+)$
RewriteCond %{REQUEST_URI} ^/baragain/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baragain,E=_ROUTING_foo:%1]

# baz
Expand All @@ -39,25 +39,25 @@ RewriteCond %{REQUEST_URI} ^/test/baz3/$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz3]

# baz4
RewriteCond %{REQUEST_URI} ^/test/([^/]+)$
RewriteCond %{REQUEST_URI} ^/test/([^/]++)$
RewriteRule .* $0/ [QSA,L,R=301]
RewriteCond %{REQUEST_URI} ^/test/([^/]+)/$
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz4,E=_ROUTING_foo:%1]

# baz5
RewriteCond %{REQUEST_URI} ^/test/([^/]+)/$
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteCond %{REQUEST_METHOD} !^(GET|HEAD)$ [NC]
RewriteRule .* - [S=2,E=_ROUTING__allow_GET:1,E=_ROUTING__allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/test/([^/]+)$
RewriteCond %{REQUEST_URI} ^/test/([^/]++)$
RewriteRule .* $0/ [QSA,L,R=301]
RewriteCond %{REQUEST_URI} ^/test/([^/]+)/$
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz5,E=_ROUTING_foo:%1]

# baz5unsafe
RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]+)/$
RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]++)/$
RewriteCond %{REQUEST_METHOD} !^(POST)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING__allow_POST:1]
RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]+)/$
RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz5unsafe,E=_ROUTING_foo:%1]

# baz6
Expand Down
Expand Up @@ -31,7 +31,7 @@ public function match($pathinfo)
}

// bar
if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
$allow = array_merge($allow, array('GET', 'HEAD'));
goto not_bar;
Expand All @@ -44,7 +44,7 @@ public function match($pathinfo)
not_bar:

// barhead
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
$allow = array_merge($allow, array('GET', 'HEAD'));
goto not_barhead;
Expand Down Expand Up @@ -72,14 +72,14 @@ public function match($pathinfo)
}

// baz4
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]+)/$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]++)/$#s', $pathinfo, $matches)) {
$matches['_route'] = 'baz4';

return $matches;
}

// baz5
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]+)/$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]++)/$#s', $pathinfo, $matches)) {
if ($this->context->getMethod() != 'POST') {
$allow[] = 'POST';
goto not_baz5;
Expand All @@ -92,7 +92,7 @@ public function match($pathinfo)
not_baz5:

// baz.baz6
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]+)/$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]++)/$#s', $pathinfo, $matches)) {
if ($this->context->getMethod() != 'PUT') {
$allow[] = 'PUT';
goto not_bazbaz6;
Expand Down Expand Up @@ -124,14 +124,14 @@ public function match($pathinfo)
if (0 === strpos($pathinfo, '/a')) {
if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo1
if (preg_match('#^/a/b\'b/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo1';

return $matches;
}

// bar1
if (preg_match('#^/a/b\'b/(?<bar>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?<bar>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar1';

return $matches;
Expand All @@ -148,14 +148,14 @@ public function match($pathinfo)

if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo2
if (preg_match('#^/a/b\'b/(?<foo1>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?<foo1>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo2';

return $matches;
}

// bar2
if (preg_match('#^/a/b\'b/(?<bar1>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?<bar1>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar2';

return $matches;
Expand All @@ -167,7 +167,7 @@ public function match($pathinfo)

if (0 === strpos($pathinfo, '/multi')) {
// helloWorld
if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?<who>[^/]+))?$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?<who>[^/]++))?$#s', $pathinfo, $matches)) {
return array_merge($this->mergeDefaults($matches, array ( 'who' => 'World!',)), array('_route' => 'helloWorld'));
}

Expand All @@ -184,14 +184,14 @@ public function match($pathinfo)
}

// foo3
if (preg_match('#^/(?<_locale>[^/]+)/b/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/(?<_locale>[^/]++)/b/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo3';

return $matches;
}

// bar3
if (preg_match('#^/(?<_locale>[^/]+)/b/(?<bar>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/(?<_locale>[^/]++)/b/(?<bar>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar3';

return $matches;
Expand All @@ -203,7 +203,7 @@ public function match($pathinfo)
}

// foo4
if (0 === strpos($pathinfo, '/aba') && preg_match('#^/aba/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/aba') && preg_match('#^/aba/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo4';

return $matches;
Expand All @@ -217,14 +217,14 @@ public function match($pathinfo)

if (0 === strpos($pathinfo, '/a/b')) {
// b
if (preg_match('#^/a/b/(?<var>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b/(?<var>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'b';

return $matches;
}

// c
if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?<var>[^/]+)$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?<var>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'c';

return $matches;
Expand Down
Expand Up @@ -31,7 +31,7 @@ public function match($pathinfo)
}

// bar
if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
$allow = array_merge($allow, array('GET', 'HEAD'));
goto not_bar;
Expand All @@ -44,7 +44,7 @@ public function match($pathinfo)
not_bar:

// barhead
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
$allow = array_merge($allow, array('GET', 'HEAD'));
goto not_barhead;
Expand Down Expand Up @@ -76,7 +76,7 @@ public function match($pathinfo)
}

// baz4
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]+)/?$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]++)/?$#s', $pathinfo, $matches)) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'baz4');
}
Expand All @@ -87,7 +87,7 @@ public function match($pathinfo)
}

// baz5
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]+)/$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]++)/$#s', $pathinfo, $matches)) {
if ($this->context->getMethod() != 'POST') {
$allow[] = 'POST';
goto not_baz5;
Expand All @@ -100,7 +100,7 @@ public function match($pathinfo)
not_baz5:

// baz.baz6
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]+)/$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?<foo>[^/]++)/$#s', $pathinfo, $matches)) {
if ($this->context->getMethod() != 'PUT') {
$allow[] = 'PUT';
goto not_bazbaz6;
Expand Down Expand Up @@ -132,14 +132,14 @@ public function match($pathinfo)
if (0 === strpos($pathinfo, '/a')) {
if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo1
if (preg_match('#^/a/b\'b/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo1';

return $matches;
}

// bar1
if (preg_match('#^/a/b\'b/(?<bar>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?<bar>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar1';

return $matches;
Expand All @@ -156,14 +156,14 @@ public function match($pathinfo)

if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo2
if (preg_match('#^/a/b\'b/(?<foo1>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?<foo1>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo2';

return $matches;
}

// bar2
if (preg_match('#^/a/b\'b/(?<bar1>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?<bar1>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar2';

return $matches;
Expand All @@ -175,7 +175,7 @@ public function match($pathinfo)

if (0 === strpos($pathinfo, '/multi')) {
// helloWorld
if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?<who>[^/]+))?$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?<who>[^/]++))?$#s', $pathinfo, $matches)) {
return array_merge($this->mergeDefaults($matches, array ( 'who' => 'World!',)), array('_route' => 'helloWorld'));
}

Expand All @@ -196,14 +196,14 @@ public function match($pathinfo)
}

// foo3
if (preg_match('#^/(?<_locale>[^/]+)/b/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/(?<_locale>[^/]++)/b/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo3';

return $matches;
}

// bar3
if (preg_match('#^/(?<_locale>[^/]+)/b/(?<bar>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/(?<_locale>[^/]++)/b/(?<bar>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar3';

return $matches;
Expand All @@ -215,7 +215,7 @@ public function match($pathinfo)
}

// foo4
if (0 === strpos($pathinfo, '/aba') && preg_match('#^/aba/(?<foo>[^/]+)$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/aba') && preg_match('#^/aba/(?<foo>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo4';

return $matches;
Expand All @@ -229,14 +229,14 @@ public function match($pathinfo)

if (0 === strpos($pathinfo, '/a/b')) {
// b
if (preg_match('#^/a/b/(?<var>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/a/b/(?<var>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'b';

return $matches;
}

// c
if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?<var>[^/]+)$#s', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?<var>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'c';

return $matches;
Expand Down
Expand Up @@ -32,7 +32,7 @@ public function match($pathinfo)
}

// dynamic
if (preg_match('#^/rootprefix/(?<var>[^/]+)$#s', $pathinfo, $matches)) {
if (preg_match('#^/rootprefix/(?<var>[^/]++)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'dynamic';

return $matches;
Expand Down

0 comments on commit 4eee88f

Please sign in to comment.