Problem/Motivation

\Drupal\Core\DependencyInjection\Container::get() is called a lot. That is as expected, but given that, it makes a measurable difference to remove the extra stack call and solve what it does (set ->serviceId) during container build and set().

Proposed resolution

  • Remove Drupal's override of get().
  • Instead, add a compiler pass to set the service id in the compiled container.
  • Also add a set() override to handle synthetic services (services set at runtime, and therefore not covered by the compiler pass).

Note, it's possible that the compiler pass, and maybe also the set() override, will be removable in #2389193: Provide a way to determine whether a given instance was retrieved from the container. But the goal of this issue is to remove the get(), that issue isn't done yet (and it's not clear when it will be), so those things are necessary for now.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Status: Needs review » Needs work

The last submitted patch, container-get-optimization.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
799 bytes
1.37 KB

I do agree that pure xhprof based benchmarking is not the right tool for the job
and ab still can show if there is a difference.

In order to understand it on the miro level I did some small benchmark: http://nbviewer.ipython.org/gist/dawehner/78c9f19cefdeaae5c9e4
The clear result is that on the micro level this is quite an improvement factor 3. This effect will be much less in reality because you have more than one service in the container.

The failure seems to be a bug in our container and the set() method.

msonnabaum’s picture

Just to clarify, ab should not be used for benchmarking like this. It introduces far too many unrelated variables.

When xhprof's overhead is enough to influence the results, like it is here, microtime is the tool to use. Just throw two microtime(true)'s in index.php and write the results to a csv file.

effulgentsia’s picture

FileSize
1.37 KB
620 bytes

How about this variation to the set() method?

Just throw two microtime(true)'s in index.php and write the results to a csv file.

And use what to generate enough entries in that CSV file to get a low enough standard deviation of the mean? In any case, do we need to get a result more precise than the issue summary's ab median? We know that PHP stack calls have overhead and that this function gets called a lot.

msonnabaum’s picture

It's fine to use ab to generate traffic, but don't use the time that it took to serve the full request as your measurement.

dawehner’s picture

FileSize
678 bytes

On top of that we can also optimize the is_object call itself, which itself saves you a couple of calls, but also wall time itself. (50 nodes on the frontpage, standard install)

=== 8.0.x..2307869-1 compared (544a67cf8ad3f..544a6be5184a9):                                                                            │
                                                                                                                                         │
ct  : 77680|75444|-2236|-2.9%                                                                                                            │
wt  : 423201|417254|-5947|-1.4%                                                                                                          │
cpu : 418208|413734|-4474|-1.1%                                                                                                          │
mu  : 23594424|23580848|-13576|-0.1%                                                                                                     │
pmu : 23673520|23659264|-14256|-0.1%                                                                                                     │
                                                                                                                                         │
-i used with no filenames on the command line, reading from STDIN.                                                                       │
<a href="http://d8.dev/xhprof-kit/xhprof/xhprof_html/index.php?source=drupal-perf&url=%2F&run1=544a67cf8ad3f&run2=544a6be5184a9&extra=8.0│
.x..2307869-1">Profiler output</a>                                                                                                       │
                                                                                                                                         │
---                                                                                                                                      │
ct = function calls, wt = wall time, cpu = cpu time used, mu = memory usage, pmu = peak memory usage         
Berdir’s picture

Measuring wall time results in considerable overhead, especially for method calls. We should measure this without that or possibly even do micro-benchmarks with manual microtime() comparisons.

Fabianx’s picture

For any micro/benchmarks I think we should setup a scenario on 3v4l.org:

http://3v4l.org/8BRYI/perf vs. http://3v4l.org/GHpAH/perf

We can see that calling the parent is still 50% slower in PHP 5.5 for example.

There have been other services that provide performance comparisons for PHP, but I don't know them.

But yes setup scenarios for microbenchmarks yourself is way better.

On the other hand if we can simply remove one function call, that is a clear yes - if its called enough times and xhprof can tell us that.

dawehner’s picture

I went with a standard D8 frontpage: 1,217 calls in total to Container::get, with the appended script I found the following difference, for the given patch:

Before

Benching function Array
Array
(
    [attempts] => 1
    [internal offset] => 0.00029736518859863
    [manual offset] => 0
    [covar] => 7.5623847092704E-6
    [stdev] => 2.0586622326846E-8
    [raw mean] => 0.0027222394943237
    [internally adjusted mean] => 0.0024248743057251
    [fully adjusted mean] => 0.0024248743057251
)

After

Benching function Array
Array
(
    [attempts] => 1
    [internal offset] => 0.00032463312149048
    [manual offset] => 0
    [covar] => 2.9162324849065E-6
    [stdev] => 2.1894493329455E-9
    [raw mean] => 0.00075078010559082
    [internally adjusted mean] => 0.00042614698410034
    [fully adjusted mean] => 0.00042614698410034
)

According to that, we indeed even save 2ms for that really simple site.
Note: 100 will dissapear alone from getting rid of hook_library_alter() per library but rathter one in total.

dawehner’s picture

Priority: Normal » Major

Note: This is with disabled xdebug/uprofiler.

Given that result, this seems to be at least major.

Status: Needs review » Needs work

The last submitted patch, 9: micro-2307869-9.patch, failed testing.

Wim Leers’s picture

Major +1.

amateescu’s picture

+++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
@@ -18,9 +18,12 @@ class Container extends SymfonyContainer {
-    if (is_object($service)) {
+    if ($service === (object) $service) {

Given that the parent can only return an object or NULL, wouldn't a strict check for NULL be faster than this type check/assignment?


+++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
@@ -18,9 +18,12 @@ class Container extends SymfonyContainer {
     $service = parent::get($id, $invalidBehavior);

If we're really down to micro-optimizing Drupal\Core\DependencyInjection\Container::get() calls and an extra stack call is such a problem (which I doubt), we can always inline the parent method's code in order to remove at least that call. Nevemind this, the first hunk of the patch achieves the same quite nicely. @effulgentsia++

Berdir’s picture

Benchmarking is hard :) Your script isn't that fair because you always call it with the same argument, so you more or less only test the first check, which means it is obviously a lot faster.

I changed it to 10k calls to see a bit more differences and tested a version without that:

HEAD:
Array
(
    [attempts] => 1
    [internal offset] => 0.00097194910049438
    [manual offset] => 0
    [covar] => 9.9728565679393E-6
    [stdev] => 1.1068450248786E-7
    [raw mean] => 0.011098575592041
    [internally adjusted mean] => 0.010126626491547
    [fully adjusted mean] => 0.010126626491547
)

Patch:
Array
(
    [attempts] => 1
    [internal offset] => 0.00097799062728882
    [manual offset] => 0
    [covar] => 3.9061859623751E-6
    [stdev] => 1.3947537619667E-8
    [raw mean] => 0.0035706281661987
    [internally adjusted mean] => 0.0025926375389099
    [fully adjusted mean] => 0.0025926375389099
)

Patch without the $this->services check:
Array
(
    [attempts] => 1
    [internal offset] => 0.00098129749298096
    [manual offset] => 0
    [covar] => 4.166603248372E-5
    [stdev] => 4.2633758766897E-7
    [raw mean] => 0.010232257843018
    [internally adjusted mean] => 0.0092509603500366
    [fully adjusted mean] => 0.0092509603500366
)

So the (object) thing seems to be slightly faster but not by much and the results vary between ~0.0092 and ~0.01, the tries stuff doesn't seem to really work, I increased it to 1000 and the results varied just as much.

Also, results from of 3v4l:
$object === (object) $object:
http://3v4l.org/dEQYC/perf#tabs

$object !== NULL:
http://3v4l.org/Hf8vq/perf#tabs

So the (object) thing is at least as fast and on some versions faster.. results seem pretty stable. A bit surprised by this :)

It wouldn't matter, because neither !== NULL nor just doing a (if ($service = parent::get()) seem to work for, I get two Attempt to assign property of non-object Container.php:24 with both versions and the microbench script and I can *not* figure out why.

Anyway, the only relevant improvement seems to be the $this->services check, and for that, it depends a lot on how many times we call the same service.. the more the more we can save ;)

Berdir’s picture

Status: Needs work » Needs review
FileSize
898 bytes

Hm, tried to fix the failing tests, but there are two problems:

a) Because the unit test calls set() with the test service, we already populate $this->services, we just return that object.
b) This also means that synthetic services that are always manually set would not be detected anymore by the dependency serialization trait?

I can think of two ways to fix this:

1. See attached patch. no longer immediately return but add an isset(), at least in xhprof, that seems to be almost as fast. And 2.5 times as fast in the microbenchmark.

2. Also set the _serviceId in set(). Didn't try that yet. That would also "fix" the unit test, but we wouldn't be testing the same as before. The only way I can think of to still test get() would be to simulate a compiled container by subclassing and manually adding a getTestService() method.

Thoughts?

amateescu’s picture

Here's another idea: we could try to push the DependencySerializationTrait solution upstream and get rid of our Container class entirely.

This overridden container was one of points from @fabpot's "list of stuff he doesn't like in the Drupal-Symfony integration" that he talked about at the Commerce 2.x sprint a few months ago, so a solution for this in Symfony 2.7 seems like a win for everyone?

areke’s picture

Issue summary: View changes
dawehner’s picture

We had good experience with symfony latetly but especially DependencySerializationTrait::__wakup() can't be backported because they don't
provide a static available container.

Here though is an alternative idea: Let's just add the service ID onto the dumped container.

After this patch the container looks like the following:

    /**
     * Gets the 'language_manager' service.
     *
     * This service is shared.
     * This method always returns the same instance of the service.
     *
     * @return \Drupal\Core\Language\LanguageManager A Drupal\Core\Language\LanguageManager instance.
     */
    protected function getLanguageManagerService()
    {
        $this->services['language_manager'] = $instance = new \Drupal\Core\Language\LanguageManager($this->get('language.default'));

        $instance->_serviceId = 'language_manager';

        return $instance;
    }

    /**
     * Gets the 'library.discovery' service.
     *
     * This service is shared.
     * This method always returns the same instance of the service.
     *
     * @return \Drupal\Core\Asset\LibraryDiscovery A Drupal\Core\Asset\LibraryDiscovery instance.
     */
    protected function getLibrary_DiscoveryService()
    {
        $a = $this->get('module_handler');

        $b = new \Drupal\Core\Asset\LibraryDiscoveryParser($this->get('app.root'), $a);
        $b->_serviceId = 'library.discovery.parser';

        $c = new \Drupal\Core\Asset\LibraryDiscoveryCollector($this->get('cache.discovery'), $this->get('lock'), $b);
        $c->_serviceId = 'library.discovery.collector';

        $this->services['library.discovery'] = $instance = new \Drupal\Core\Asset\LibraryDiscovery($c, $a);

        $instance->_serviceId = 'library.discovery';

        return $instance;
    }

As you see, also public: false services are supported.

dawehner’s picture

FileSize
2.88 KB

Ha, that was a good teaser :P

Status: Needs review » Needs work

The last submitted patch, 19: 2307869-19.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
3.35 KB
1.22 KB

Mh, so it turns out, that we have with the app.root string a problem, so let's check before we set the property.

Status: Needs review » Needs work

The last submitted patch, 21: 2307869-21.patch, failed testing.

tstoeckler’s picture

  1. +++ b/core/core.services.yml
    @@ -390,6 +390,9 @@ services:
    +    is_class: false
    

    Is that a Symfony-specific thing? Otherwise I don't see this being used in the patch.

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php
    @@ -22,7 +22,9 @@ class DependencySerializationTraitPass implements CompilerPassInterface {
    +      if (!$definition->hasTag('non_class')) {
    +        $definition->setProperty('_serviceId', $service_id);
    +      }
    

    It is unfortunate that Symfony does not check something like is_object() itself before setting properties on a service. Or are Symfony services always objects?

dawehner’s picture

Is that a Symfony-specific thing? Otherwise I don't see this being used in the patch.

Oh right, we should remove that, this was just an experiment I made.

It is unfortunate that Symfony does not check something like is_object() itself before setting properties on a service. Or are Symfony services always objects?

At least according to ContainerInterface::get() they are. One alternative way I tried out was to introduce a AppRoot class which just stores the string,
and at the same time, implements ::__toString().

tstoeckler’s picture

At least according to ContainerInterface::get() they are. One alternative way I tried out was to introduce a AppRoot class which just stores the string,
and at the same time, implements ::__toString().

Ahh, I see. Thanks. That's not for this issue then, so that's fine for now.

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.49 KB
2.3 KB

We have to be aware of the fact that we can't just unserialize private services ...

Berdir’s picture

I'm not sure what exactly were are testing in the unit test now? Can we unit test the compiler pass class instead and check for all variants there?

I think this needs another round of profiling, not sure if the microbench script is still useful or if we should just do an xhprof check of a full page, but I assume this is as fast as it can get, nice work!

Berdir’s picture

Status: Needs review » Needs work

Ah, as discussed in IRC, what I think we should overwrite now is set(), for synthetic services. Surprised that we're not seeing any failures but I guess they are usually not directly injected in objects that get serialized.

znerol’s picture

According to people in #symfony IRC, synthetic services are not useful for anything beside the kernel, class_loader and service_container. This was different before request was removed from the container. From that discussion I got the impression that synthetic services are going away rather sooner than later. Some people even thought that synthetic services were already deprecated, and nobody actually could think of a use-case other than the ones named before. Rather than add support for synthetic services in corner cases, I believe we better should deprecate them right away in Drupal.

Berdir’s picture

Yes, and how would you want to replace those three?

It doesn't matter if there's just one and/or they are deprecated. If you, for some reason, inject the kernel into a class that gets serialized, it *will* break.

znerol’s picture

Yes, and how would you want to replace those three?

Special case them, like we do with the container already (see Container::__sleep()).

dawehner’s picture

Ah, as discussed in IRC, what I think we should overwrite now is set(), for synthetic services. Surprised that we're not seeing any failures but I guess they are usually not directly injected in objects that get serialized.

Valid point.

Status: Needs review » Needs work

The last submitted patch, 32: 2307869-32.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.21 KB
7.77 KB

Alright.

Berdir queued 34: 2307869-34.patch for re-testing.

Berdir’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php
@@ -0,0 +1,34 @@
+      // Some services might have strings internally.
+      // Given that you can just reload a service which is accessible via
+      // Container::get, you need to filter out public services here.

just = only?

Trying to rewrite this a bit...

"Only add the property to services that are public (as private services can not be reloaded through Container::get()) and are objects." ?

As mentioned in #2380293: Properly inject services into ModuleInstaller, if we want to implement set() like we currently do, then we can remove the custom properties that are set over there.

dawehner’s picture

"Only add the property to services that are public (as private services can not be reloaded through Container::get()) and are objects." ?

+1

amateescu’s picture

Shouldn't we focus on #2389193: Provide a way to determine whether a given instance was retrieved from the container instead? That looks like a better solution to me.

Berdir’s picture

Status: Needs work » Needs review
FileSize
7.78 KB
1.11 KB

@amateescu: I'm not sure that is really a better option.

Here's a reroll with an improved comment.

Status: Needs review » Needs work

The last submitted patch, 39: 2307869-39.patch, failed testing.

Berdir queued 39: 2307869-39.patch for re-testing.

effulgentsia’s picture

Title: Optimize Drupal's Container::get() » Remove Drupal's Container::get()
Issue summary: View changes
Status: Needs work » Needs review

Patch looks great to me. Updated title and summary. I'm tempted to RTBC. Are there any remaining concerns that should stop me from doing that?

Berdir’s picture

I think the only thing is the issue referenced in #38.

I would prefer to get this in anyway, this is definitely an improvement and I'm not sure if the other issue really will be or if it will work, there are still a lot of test fails to figure out AFAIK.

effulgentsia’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Ok, noted that in the summary. Thanks. I agree with not holding this up on that, so RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/core.services.yml
@@ -442,6 +442,8 @@ services:
     class: SplString
     factory_service: 'app.root.factory'
     factory_method: 'get'
+    tags:
+      - { name: non_class }

class: SplSptring... tag is non_class. I think the tag could be better named - perhaps something about the effect that it actually has.

dawehner’s picture

We went with this more generic name as there might be more cases where it matters in the future.

effulgentsia’s picture

The issue is that class: SplSptring is a lie, but I suspect is needed to satisfy Symfony ContainerBuilder requirements. app.root.factory.get() returns a plain PHP string. So at a minimum, perhaps a comment in the services.yml explaining that?

But also, we'll want this tag on services that are class objects but that disallow (e.g., via __set()) setting an undeclared public property. Not sure what a good tag name to capture that is.

RavindraSingh’s picture

@alexpott, I have added the tag as mentioned below and as SplString is a class has been assigned to name.

app.root:
    class: SplString
    factory_service: 'app.root.factory'
    factory_method: 'get'
    tags:
      - { name: SplString, tag: 'non_class'}
RavindraSingh’s picture

FileSize
6.46 KB

Adding interdiff also.

RavindraSingh’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 48: 2307869-remove-drupal-container-get.patch, failed testing.

RavindraSingh’s picture

Showing Drupal installation failed after adding.

 tags:
      - { name: SplString, tag: 'non_class'}

So I have reverted it to previous stag.

RavindraSingh’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 52: 2307869-2-remove-drupal-container-get.patch, failed testing.

dawehner’s picture

class: SplSptring... tag is non_class. I think the tag could be better named - perhaps something about the effect that it actually has.

What about mutable_parameter or parameter_service ?

dawehner’s picture

Status: Needs work » Needs review
FileSize
206.31 KB
1.32 KB

Let's use parameter_service. Note: This patch starts from #39

Status: Needs review » Needs work

The last submitted patch, 56: 2307869-56.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
7.8 KB

Meh.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 58: 2307869-57.patch, failed testing.

Status: Needs work » Needs review

catch queued 58: 2307869-57.patch for re-testing.

JeroenT’s picture

Status: Needs review » Reviewed & tested by the community

RTBC as per #59.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue is a major task that will improve performance significantly and the disruption it introduces is limited. Per https://www.drupal.org/core/beta-changes, this is a good change to complete during the Drupal 8 beta phase. Committed 9cf83e9 and pushed to 8.0.x. Thanks!

  • alexpott committed 9cf83e9 on 8.0.x
    Issue #2307869 by dawehner, Berdir, RavindraSingh, effulgentsia: Remove...

Status: Fixed » Closed (fixed)

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