Problem/Motivation

In an API-First context, typed data is used A LOT so any micro optimization goes a long way. During profiling in JSON:API i found strlen to be executed too many times. It turned out that the method was used to check whether we have a prefix or not, so we can have that changed to an empty() call instead.

Proposed resolution

I've changed this:

return (strlen($prefix) ? $prefix . '.' : '') . $this->name;

to this

return strlen($prefix) ? "{$prefix}.{$this->name}" : $this->name;

to leverage fast string concatenation - https://blog.blackfire.io/php-7-performance-improvements-encapsed-string...

Remaining tasks

Review, RTBC, commit.

User interface changes

None.

API changes

None.

Data model changes

None.

Release notes snippet

None.

Comments

ndobromirov created an issue. See original summary.

ndobromirov’s picture

Status: Active » Needs review
StatusFileSize
new740 bytes
ndobromirov’s picture

Issue summary: View changes
ndobromirov’s picture

Issue tags: +API-First Initiative
ndobromirov’s picture

Issue summary: View changes
wim leers’s picture

With the change in place, and small tweaks to leverage fast string concatenation in PHP 7, I've managed to shed 17-18 ms out of the method execution time.

Holy crap! Micro optimizations that actually matter.

ndobromirov’s picture

Issue summary: View changes
wim leers’s picture

Status: Needs review » Reviewed & tested by the community

I meant to do this.

@ndobromirov Can you specify which version of PHP 7 you profiled against?

tstoeckler’s picture

Status: Reviewed & tested by the community » Needs review

If we want to go for performance I would think that !$prefix would be even faster, but not sure.

Also, more importantly, this is a behavior change if $prefix is '0'. Since I don't see any discussion of that, I'm not sure whether or not this can actually happen here, so moving back to needs review.

ndobromirov’s picture

PHP version is 7.2, but the fast string concatenation landed in 7.0.

#9 So instead of checking for empty, we can check for isset on the zero-th character. This will work for the "0" case.

My train of thought is to avoid strlen that is linear based on the prefix's length. And move that to a PHP native opcode operation that will work in a constant time. Putting a new patch shortly.

!$prefix Still does not handle the zero scenario.

Another thing, is the prefix '0' a valid case? There is no test for it, even though it's a proper edge case...

ndobromirov’s picture

StatusFileSize
new743 bytes

Here is the new version. No benchmarks. I am expecting similar performance.

wim leers’s picture

@tstoeckler FYI: I felt comfortable RTBC'ing this since the entire test suite passes.

ndobromirov’s picture

If that's not ok, we can always compare to empty string with $prefix === '' , as it might be more readable.

borisson_’s picture

Are we sure that the increase in speed is from the change of strlen? Or is it because of the change in string concatenation?

Because if it is the latter, that seems a whole lot less controversial.

In any case, if #9 is a valid prefix, we should probably add a test for that as well, so we can be sure that this new patch doesn't break that behavior?

ndobromirov’s picture

Even if it's not, it's 16k+ less function calls in my current use case, so if possible I will prefer not having to call strlen.

It seems there is already tests to cover that. No idea why it did not trigger:

    // Test validating a list of a values and make sure property paths starting
    // with "0" are created.
    $definition = BaseFieldDefinition::create('integer');
    $violations = $this->typedDataManager->create($definition, [['value' => 10]])->validate();
    $this->assertEqual($violations->count(), 0);
    $violations = $this->typedDataManager->create($definition, [['value' => 'string']])->validate();
    $this->assertEqual($violations->count(), 1);

    $this->assertEqual($violations[0]->getInvalidValue(), 'string');
    $this->assertIdentical($violations[0]->getPropertyPath(), '0.value');

Anyone can take over writing the tests.

ndobromirov’s picture

StatusFileSize
new90.86 KB
new129.23 KB

Here is a benchmark with the strlen() and just the concatenation fixes - small wins.

strlen is still executed ~18k times more than its needed.

And strlen view

berdir’s picture

Just following for now and one comment. Keep in mind that profiling adds overhead and that overhead gets bigger with a lot of calls to fast functions. I'd imagine the improvement might be a lot less if you do a performance test without xdebug. for example just call this method many times in a loop and then check the time it took with a microtime() call before + after.

xhprof (or whatever this is exactly) might have less overhead, but I know that for example blackfire can increase the exeuction times by 200-300%.

ndobromirov’s picture

There is no xdebug - just the profiler - xhpof. The overhead is roughly doubling the execution time. The percentage improvement will stay the same with and without xhprof enabled and that is around 35% for this function with the proposed changes.

berdir’s picture

> The overhead is roughly doubling the execution time.

Yes, but my point is that the overhead is bigger for fast functions, so yes, it will still be 35% faster than before, but this specific function might be reported as 3-4 or more times slower due to the overhead and not just 50%. Plus, empty()/isset() and array access is not a regular function and I guess is not tracked at all by xhprof. So you can't just compare how much time you have less on strlen(), but instead compare the execution time of the function.

So instead, I would try to compare it without xhprof and just do a PHP script that calls strlen() a million times or so and compare the passed time.

I did find https://stackoverflow.com/questions/6955913/isset-vs-strlen-a-fast-clear..., which does report that isset() is 6x faster *but* that was in 2011. There is a comment that says that the performance difference is only 10% on PHP 7.1 with 10k iterations (interestingly, that reported the difference got bigger with more iterations).

That is contradicted by this comment on strlen: http://php.net/manual/de/function.strlen.php#120632.

Just like the comments on the stackexchange question say, the new code is harder to understand, so my point is that we should only do this if we are *sure* that there really is a measurable performance difference.

PS: xdebug was a typo in my previous comment, should have said xhprof in both cases.

wim leers’s picture

@Berdir++

Alternatively, disable caching and do 1K requests with and without the patch (and report variance) with ab. IOW: do benchmarking of a repeatable use case. If the performance difference is noticeable, it should show up there too.

ndobromirov’s picture

StatusFileSize
new741 bytes

Yup... They are performance wise the same. I did not know that but PHP stored a counter for the length, so strlen is constant - O(1) as well with that in mind I will pass a patch only with the concatenation tweaks.

Based on a local script after 5 mil iterations it turned out that strlen is actually faster than isset()[0] with a margin of 0.3E-6 over the whole execution time (not average) :D.

borisson_’s picture

Ok, that means we don't need tests, we're not changing anything functional anymore. We're only doing a different kind of concatenation now.

We should probably update the Summary of this issue to indicate that we're not longer looking at strlen/empty to make this faster. So that someone reading the issue description doesn't get confused with the patch :)

wim leers’s picture

So what is the real-world performance gain that's we're observing then?

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

joseph.olstad’s picture

Status: Needs review » Reviewed & tested by the community

Patch applies cleanly on latest head of 9.1.x - tests triggered
Patch applies cleanly on latest head of 9.0.x - tests triggered
Patch applies cleanly on latest head of 8.9.x
Patch applies cleanly on latest head of 8.8.x

This is a win, I'll take any performance gain I can get with no harm to functionality, super!

catch’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs issue summary update

The discussion here indicates this is no longer being done for performance (since strlen() is no longer slower than isset()), and it doesn't remove the function call.

The new version of the code is more readable - is that the only improvement here?

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

alexpott’s picture

Re #28 well according to https://blog.blackfire.io/php-7-performance-improvements-encapsed-string... the patch here will be able leverage fast string concatenation while HEAD does not. Also in case of no prefix we are no longer doing concatenation. SO I guess we can do this. It certainly is not going to hurt.

joseph.olstad’s picture

Triggerred patch against 9.5.x, 9.4.x and 9.3.x ALL GREEN

alexpott’s picture

Status: Needs work » Reviewed & tested by the community

Once getPropertyPath() has a return typehint of string we can change the strlen() to !== '' which will be a saving if this is called thousands of times. There is a value in removing this from profiles as it might make other things easier to spot. I doubt the gains a big at all here. But they will be bigger than 0.

alexpott’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Fixing issue summary.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

So I profiled this with the following script:


class test {
  protected $name = 'test';

  public function oldf($prefix) {
    return (strlen($prefix) ? $prefix . '.' : '') . $this->name;
  }

  public function newf($prefix) {
    return strlen($prefix) ? "{$prefix}.{$this->name}" : $this->name;
  }
}

$tests = [];
for ($i = 0; $i < 100000; $i++) {
  $tests[] = random_bytes(256);
}
var_dump(count($tests));
$test = new Test();
$name = 'test';
$start = microtime(TRUE);
foreach ($tests as $string) {
  $test->newf($string);
}
$end = microtime(TRUE);
print "New function: " . ($end - $start) * 1000 . "ms\n";


reset($tests);
$start = microtime(TRUE);
foreach ($tests as $string) {
  $test->oldf($string);
}
$end = microtime(TRUE);
print "Old function: " . ($end - $start) * 1000 . "ms\n";

The new function seems to come out on tops more often than not but it is a bit random.

I then profiled a version of the function like return $prefix !== '' ? "{$prefix}.{$this->name}" : $this->name; and that was always quicker.

I did the profiling on PHP 8.1 so maybe there are further improvements to string concatenation in PHP 7 or PHP 8 that make this change not important. I think we should retask this to do the no string len at all.

joseph.olstad’s picture

Status: Needs work » Needs review
StatusFileSize
new5.64 KB

new patch as per comment #36

joseph.olstad’s picture

StatusFileSize
new740 bytes

Oops on patch 37, new patch.

The last submitted patch, 37: 3017710-37.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 38: 3017710-38.patch, failed testing. View results

joseph.olstad’s picture

Status: Needs work » Needs review

Triggered a couple

berdir’s picture

\MoveBlockFormTest::testMoveBlock might be a random file, I wouldn't be so sure about the other one. Layout Builder definitely does some funky stuff with entities and fields, wouldn't be urprised if that's a real issue.

@joseph.olstad: You might want to have a look at git apply -3, that will create an inline merge conflict like a git merge and not .rej and .orig files.

joseph.olstad’s picture

tests for #38 are green for 9.3.x and 9.4.x
patch is verbatim as designed by @alexpott from #36

@berdir, yes thanks for that suggestion I will try git apply -3 next time and see what it does.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Triggering for 10.1 but was this waiting on anything else? Reading the comments and seemed to leave off on the test failures. Can wait to see what 10.1 does.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

No D10 failures.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 91e19e5 and pushed to 10.1.x. Thanks!

  • alexpott committed 91e19e5c on 10.1.x
    Issue #3017710 by ndobromirov, joseph.olstad, Wim Leers, alexpott,...
phenaproxima’s picture

One small thing that should maybe be done in this issue as a small follow-up patch: there really should be a comment there explaining why we're using the constructs we're using, and that it was specifically done for performance reasons. Otherwise we risk regressing it in the future, since we're not (AFAIK) doing regular profiling of core.

quietone’s picture

Issue tags: +Needs followup

Adding tag for a followup per #50

Status: Fixed » Closed (fixed)

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

quietone’s picture