orm: Significant performance degradation with `enable_lazy_ghost_objects: true`

Bug Report

Q A
BC Break no
Version 2.17.1

Summary

After updating ORM from 2.15.2 to 2.17.1 and turning on enable_lazy_ghost_objects as it’s suggested I’ve noticed a significant performance degradation during test suite running. The overall suite became slower by noticeable 30%. Localizing the slow spot I’ve found that ProxyReferenceRepository::load() now takes 50 seconds in total instead of the previous 3.86 seconds, i.e. became 13x slower. Tracking down ProxyReferenceRepository::load() -> ProxyReferenceRepository::unserialize() I’ve found that EntityManager::getReference() became 23x slower (2.055 seconds -> 47.888 seconds in total). This real project has hundreds of entities and complex relations between them. During running test suite with 1k+ scenarios a lot of fixtures are generated, saved and loaded (including references):

Time in seconds enable_lazy_ghost_objects: false enable_lazy_ghost_objects: true Slowdown
EntityManager::getReference() 2.055900 47.887696 23.29x
UnitOfWork::tryGetById() 0.277827 9.693281 34.89x
ProxyFactory::getProxy() 0.751345 27.942798 37.19x
UnitOfWork::registerManaged() 0.611383 9.814827 16.05x

I’ve created a test command to reproduce the issue, and running it in the same project I get:

Time in seconds enable_lazy_ghost_objects: false enable_lazy_ghost_objects: true Slowdown
Total 1.922614 8.043023 4.18x
EntityManager::getReference() 1.784766 7.948818 4.45x
UnitOfWork::tryGetById() 0.232570 0.254660 1.09x
ProxyFactory::getProxy() 0.605608 4.091646 6.76x
UnitOfWork::registerManaged() 0.547479 3.200220 5.85x

I’ve created a small reproducer: https://github.com/javer/doctrine-orm-lazy-ghost It’s a fresh Symfony 6.3 project with one additional commit: https://github.com/javer/doctrine-orm-lazy-ghost/commit/36a3e20b93fe5dba1a520a24351ad794477185e9 Running bin/console app:test:orm I have the following results:

Time in seconds enable_lazy_ghost_objects: false enable_lazy_ghost_objects: true Slowdown
Total 2.388989 4.273117 1.79x
UnitOfWork::tryGetById() 0.340158 0.349181 1.03x
ProxyFactory::getProxy() 0.709970 2.551538 3.59x
UnitOfWork::registerManaged() 0.776654 0.818202 1.05x

Slowdown numbers are not so high as in a real project, but there is only one simple entity without any relations, and no save/load operations, only getting references. Even in this simple case ProxyFactory::getProxy() slowdown by almost 4x is too expensive for using enable_lazy_ghost_objects.

Taking into account that enable_lazy_ghost_objects will be always true in ORM 3.0 I think it’s worth fixing it.

How to reproduce

git clone https://github.com/javer/doctrine-orm-lazy-ghost
cd doctrine-orm-lazy-ghost
composer install
bin/console app:test:orm

Expected behavior

Turning on enable_lazy_ghost_objects shouldn’t have a noticeable impact on the performance.

About this issue

  • Original URL
  • State: closed
  • Created 7 months ago
  • Reactions: 11
  • Comments: 19 (16 by maintainers)

Commits related to this issue

Most upvoted comments

Here is a screenshot of a compare callgraph in Tideways Profiler, it shows the relevant parts that are slow:

  • createLazyGhost static factory
  • The callback returned from ProxyFactory::getProxyFactory

The overall slowdown is roughly in line what @rrehbein reported.

Bildschirmfoto 2024-02-22 um 13 47 25

I will take a deeper look when i have more time.

I looked at createLazyGhost factory method and found that it acts both as an initializer when invoked the first time, and then as a factory for lazy ghosts. It seems when the initializer code is run every time as well, this causes a lot of wasted CPU and allocated memory. A simple attempt to split both methods up cut 50% of the slowdown:

Bildschirmfoto 2024-02-25 um 12 11 42

I hardcoded it into my doctrine/orm and symfony/var_exporter components of @javer testproject. So no nice diff, the changed lines are here: https://gist.github.com/beberlei/97899ea7ce4261502a6df1300ab9f181

This would require var-exporter to add these new methods, so I am looking to find a simpler fix that cuts the time and memory in createLazyGhost already.

Below is a better patch for the ORM side. Together with this one https://github.com/symfony/symfony/pull/54253, I get the following stats on my machine:

  • lazy-ghosts disabled: 2.94 s
  • lazy-ghosts patched: 4.02 s
  • lazy-ghosts unpatched: 7.83 s

Locally, I also have a native proxy implementation for php-src (I need to write an RFC about it), and with it, I get 3.27s with the current API and implementation. That’s good food for thoughts on my side to move this RFC forward 😃

Here is the Blackfire.io comparison between disabled and patched lazy-ghosts: https://blackfire.io/profiles/compare/4561cec4-97af-4c40-a180-ee5745ca8119/graph

image

The patch for ProxyFactory.php
--- a/src/Proxy/ProxyFactory.php
+++ b/src/Proxy/ProxyFactory.php
@@ -360,11 +360,11 @@ EOPHP;
      */
     private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
     {
-        return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
-            $identifier = $classMetadata->getIdentifierValues($original);
-            $entity     = $entityPersister->loadById($identifier, $original);
+        return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
+            $identifier = $classMetadata->getIdentifierValues($proxy);
+            $original   = $entityPersister->loadById($identifier);
 
-            if ($entity === null) {
+            if ($original === null) {
                 throw EntityNotFoundException::fromClassNameAndIdentifier(
                     $classMetadata->getName(),
                     $identifierFlattener->flattenIdentifier($classMetadata, $identifier)
@@ -382,7 +382,7 @@ EOPHP;
                     continue;
                 }
 
-                $property->setValue($proxy, $property->getValue($entity));
+                $property->setValue($proxy, $property->getValue($original));
             }
         };
     }
@@ -468,9 +468,7 @@ EOPHP;
         $identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
 
         $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
-            $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
-                $initializer($object, $proxy);
-            }, $skippedProperties);
+            $proxy = self::createLazyGhost($initializer, $skippedProperties);
 
             foreach ($identifierFields as $idField => $reflector) {
                 if (! isset($identifier[$idField])) {

This is good enough to me. […] The legacy proxy system has many serious limitations.

I don’t doubt that the new setup is better, and I’m sure eventually it will catch up, but right now we’re seeing the same results as you outline in https://github.com/doctrine/orm/issues/11087#issuecomment-1994002826, so losing half the performance (from 17s to 29s for setting up a subset of test fixtures).

That’s many times better than before your patch, of course, but still a fairly high cost… I hope the issue keeps getting some attention until we’re in a closer performance range.

EDIT: turns out I was comparing on 2.19.2 which lacks the patch; using 2.19.3 seems to fix the issue entirely for our use-case (at least with synthetic loads, we’ll have to compare for real load).

I can confirm that after upgrading to doctrine/orm 2.19.3 turning on enable_lazy_ghost_objects makes the initial test suite slower only by negligible 5%.

Thank you very much @beberlei and @nicolas-grekas !

turns out I was comparing on 2.19.2 which lacks the patch; using 2.19.3 seems to fix the issue entirely for our use-case (at least with synthetic loads, we’ll have to compare for real load).

For what it’s worth, we also see no performance difference at all after deploying to our production: image

So I can only agree with the decision to close it 👍