Problem/Motivation

strtr() is slow, compared to str_replace()

UrlGenerator::doGenerate() uses it for no actual reason.

Proposed resolution

Use str_replace()

Remaining tasks

User interface changes

API changes

Data model changes

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task because nothing is broken.
Issue priority Normal, because just a little micro-optimization, but micro-optimizations can also add up.
Prioritized changes The main goal of this issue is performance.
Disruption Not disruptive for core/contributed and custom modules/themes.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dawehner’s picture

Status: Active » Needs review
FileSize
4.69 KB
Function Name Calls Diff Calls
Diff%
Incl. Wall
Diff
(microsec)
IWall
Diff%
Incl.
MemUse
Diff
(bytes)
IMemUse
Diff%
Incl.
PeakMemUse
Diff
(bytes)
IPeakMemUse
Diff%
Current Function
Drupal\Core\Routing\UrlGenerator::doGenerate 0 0.0% -36 -0.1% 1,648 5.7% -159,248 -552.6%
Exclusive Metrics Diff for Current Function 29 80.6% -1,792 -108.7% 1,544 1.0%
Parent function
Drupal\Core\Routing\UrlGenerator::getInternalPathFromRoute 0 N/A% -36 -100.0% 1,648 100.0% -159,248 -100.0%
Child functions
strtr -4 -50.0% -78 -216.7% -1,032 -62.6% -162,848 -102.3%
strpos 0 0.0% 9 25.0% 0 0.0% 192 0.1%
array_replace 0 0.0% 3 8.3% 0 0.0% -80 -0.1%
str_replace 4 50.0% 1 2.8% 784 47.6% 568 0.4%
rawurlencode 0 0.0% 0 0.0% 0 0.0% 80 0.1%
array_keys 4 50.0% 0 0.0% 2,000 121.4% 616 0.4%
Symfony\Component\Routing\RequestContext::getParameters 0 0.0% 0 0.0% 8 0.5% 32 0.0%
array_diff_key 0 0.0% 0 0.0% 0 0.0% 0 0.0%
array_flip 0 0.0% 0 0.0% 0 0.0% 0 0.0%
array_values 4 50.0% 0 0.0% 1,680 101.9% 648 0.4%
dawehner’s picture

Good catch fabianx!

The last submitted patch, 1: 2538982-1.patch, failed testing.

Fabianx’s picture

Issue summary: View changes

Micro-optimization, but it is well known that str_replace is faster than strtr, so lets do that.

Wim Leers’s picture

Should we also file an upstream issue?

Wim Leers’s picture

  1. +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
    @@ -54,14 +54,16 @@ class UrlGenerator implements UrlGeneratorInterface {
    +   * The format are the two first parameter of str_replace().
    

    This is difficult to parse.

  2. +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
    @@ -54,14 +54,16 @@ class UrlGenerator implements UrlGeneratorInterface {
    +    ['%2F'], ['/']
    

    Can we put these on separate lines, to remove the visual suggestion that this is a key-value map?

pwolanin’s picture

Maybe better to use a list() construct here? I think it's clearer to read.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Sure, why not.

Wim Leers’s picture

RTBC+1

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -212,7 +215,8 @@ protected function doGenerate(array $variables, array $defaults, array $tokens,
-    $url = strtr(rawurlencode($url), $this->decodedChars);
+    list($search_strings, $replacement_strings) = $this->decodedChars;
+    $url = str_replace($search_strings, $replacement_strings, rawurlencode($url));

So what's actually faster... if we're doing a micro-optimisation adding an extra operation seem interesting.

Fabianx’s picture

#10:

"Like array(), this is not really a function, but a language construct. list() is used to assign a list of variables in one operation."

(http://php.net/manual/en/function.list.php)

So this should be identical to using:

$search_strings = $this->decodedChars[0];
$replacement_strings = $this->decodedChars[1];

However I benchmarked the cases and this is the result:

str_tr: 5.160556s 0.000000s
str_replace: 1.017935s 0.000000s
str_replace_variables: 1.051348s 0.000000s
str_replace_list: 1.055539s 0.000000s
str_replace_single: 0.663118s 0.000000s
str_replace_list_single: 0.920595s 0.000000s
str_replace_single_isset: 0.940557s 0.000000s

with the following scenarios (xhprof off):

$test_bases['str_tr'] = function() {
  $url = 'foo/bar/bla';
  $decodedChars = [
    '%2F' => '/',
  ];
  $url = strtr(rawurlencode($url), $decodedChars);
};

$test_bases['str_replace'] = function() {
  $url = 'foo/bar/bla';
  $decodedChars = [
    ['%2F'], // Map from these encoded characters.
    ['/'], // Map to these decoded characters.
  ];
  $url = str_replace($decodedChars[0], $decodedChars[1], rawurlencode($url));
};

$test_bases['str_replace_variables'] = function() {
  $url = 'foo/bar/bla';
  $decodedChars = [
    ['%2F'], // Map from these encoded characters.
    ['/'], // Map to these decoded characters.
  ];

  $search_strings = $decodedChars[0];
  $replacement_strings = $decodedChars[0];
  $url = str_replace($search_strings, $replacement_strings, rawurlencode($url));
};

$test_bases['str_replace_list'] = function() {
  $url = 'foo/bar/bla';
  $decodedChars = [
    ['%2F'], // Map from these encoded characters.
    ['/'], // Map to these decoded characters.
  ];
  list($search_strings, $replacement_strings) = $decodedChars;
  $url = str_replace($search_strings, $replacement_strings, rawurlencode($url));
};

$test_bases['str_replace_single'] = function() {
  $url = 'foo/bar/bla';
  $decodedChars = [
    '%2F', // Map from these encoded characters.
    '/', // Map to these decoded characters.
  ];
  $url = str_replace($decodedChars[0], $decodedChars[1], rawurlencode($url));
};

$test_bases['str_replace_list_single'] = function() {
  $url = 'foo/bar/bla';
  $decodedChars = [
    ['%2F'], // Map from these encoded characters.
    ['/'], // Map to these decoded characters.
  ];
  list($search_strings, $replacement_strings) = $decodedChars;
  $url = str_replace($search_strings[0], $replacement_strings[0], rawurlencode($url));
};

$test_bases['str_replace_single_isset'] = function() {
  $url = 'foo/bar/bla';
  $decodedChars = [
    ['%2F'], // Map from these encoded characters.
    ['/'], // Map to these decoded characters.
  ];
  $search_strings = $decodedChars[0];
  $replacement_strings = $decodedChars[0];
  if (!isset($search_strings[1])) {
    $url = str_replace($search_strings[0], $replacement_strings[0], rawurlencode($url));
  }
  else {
    $url = str_replace($search_strings, $replacement_strings, rawurlencode($url));
  }
};

So yes, list() is slightly slower than directly using decodedChars[0] and decodedChars[1], but is the same when using temporary variables.

Obviously the single case is fasted, but any optimization is likely not worth the additional complexity of the isset(), etc. and part of that is also saving one array traversal.

So it is probably just the 5x improvement over str_tr that really matters here - unless we never have a use-case to support more than one encoded character.

dawehner’s picture

FileSize
1.66 KB
1.87 KB

Thank you fabian! Good to learn that.

$test_bases['str_replace_single'] = function() {
  $url = 'foo/bar/bla';
  $decodedChars = [
    '%2F', // Map from these encoded characters.
    '/', // Map to these decoded characters.
  ];
  $url = str_replace($decodedChars[0], $decodedChars[1], rawurlencode($url));
};

Given with that we support both cases I would suggest to go with that, just to add it to the documentation that you can also use arrays. It is quite an edge case to override it though.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Oh, that is a very sweet solution :).

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Thanks everyone! Committed 3555e2b and pushed to 8.0.x. Thanks!

Thank you for adding the beta evaluation to the issue summary.

  • alexpott committed 3555e2b on 8.0.x
    Issue #2538982 by dawehner, pwolanin, Fabianx: Get rid of strtr in...
Fabianx’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.