Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance optimization #387

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

Performance optimization #387

wants to merge 12 commits into from

Conversation

samdark
Copy link
Member

@samdark samdark commented Feb 9, 2025

Before:

\Yiisoft\Di\Tests\Benchmark\ContainerMethodHasBench

    benchPredefinedExisting.................R1 I2 - Mo132.604μs (±1.14%)
    benchUndefinedExisting..................R1 I2 - Mo124.260μs (±0.57%)
    benchUndefinedNonexistent...............R1 I4 - Mo455.709μs (±0.86%)

\Yiisoft\Di\Tests\Benchmark\ContainerBench

    benchConstruct..........................R1 I4 - Mo320.728μs (±1.86%)
    benchSequentialLookups # 0..............R3 I3 - Mo620.365μs (±0.44%)
    benchSequentialLookups # 1..............R3 I4 - Mo643.744μs (±0.72%)
    benchSequentialLookups # 2..............R1 I0 - Mo644.136μs (±1.16%)
    benchRandomLookups # 0..................R1 I4 - Mo623.952μs (±0.69%)
    benchRandomLookups # 1..................R1 I4 - Mo649.820μs (±1.61%)
    benchRandomLookups # 2..................R2 I4 - Mo643.426μs (±1.05%)
    benchRandomLookupsComposite # 0.........R2 I4 - Mo2.315ms (±0.71%)
    benchRandomLookupsComposite # 1.........R2 I4 - Mo2.330ms (±1.03%)
    benchRandomLookupsComposite # 2.........R1 I4 - Mo2.317ms (±1.05%)

After:

\Yiisoft\Di\Tests\Benchmark\ContainerMethodHasBench

    benchPredefinedExisting.................R5 I4 - Mo88.505μs (±1.04%)
    benchUndefinedExisting..................R2 I2 - Mo86.247μs (±1.94%)
    benchUndefinedNonexistent...............R2 I4 - Mo454.755μs (±1.42%)

\Yiisoft\Di\Tests\Benchmark\ContainerBench

    benchConstruct..........................R2 I4 - Mo219.828μs (±2.02%)
    benchSequentialLookups # 0..............R1 I0 - Mo494.586μs (±0.64%)
    benchSequentialLookups # 1..............R1 I4 - Mo541.868μs (±0.67%)
    benchSequentialLookups # 2..............R1 I4 - Mo540.820μs (±2.01%)
    benchRandomLookups # 0..................R1 I1 - Mo520.341μs (±0.51%)
    benchRandomLookups # 1..................R1 I4 - Mo541.799μs (±1.23%)
    benchRandomLookups # 2..................R1 I0 - Mo547.495μs (±1.06%)
    benchRandomLookupsComposite # 0.........R1 I4 - Mo2.215ms (±1.22%)
    benchRandomLookupsComposite # 1.........R1 I1 - Mo2.170ms (±1.19%)
    benchRandomLookupsComposite # 2.........R1 I2 - Mo2.174ms (±0.65%)

samdark and others added 7 commits February 9, 2025 23:22
…first

- Reorder checks to prioritize definition lookup which is the most common case
- Return true immediately if definition exists
- Only check tag aliases if definition doesn't exist
- Return false if neither exists

Performance improvements:
- benchPredefinedExisting: ~29% faster (130μs → 92μs)
- benchUndefinedExisting: ~29% faster (123μs → 87μs)
- benchUndefinedNonexistent: ~1% faster (446μs → 441μs)
- Small improvements in other benchmarks
- Add fast path for existing instances
- Extract StateResetter logic into separate method
- Move StateResetter check to the end to avoid duplicate instance access
- Improve code readability and maintainability

Performance improvements:
- benchSequentialLookups: ~3% faster (620μs → 599μs)
- benchUndefinedExisting: ~5% faster (87μs → 83μs)
- Other benchmarks show similar or slightly improved performance
- Add early returns for common simple cases (strings, ContainerInterface, Closure)
- Simplify array definition handling
- Improve code readability and reduce nesting

Performance improvements:
- benchConstruct: ~29% faster (336μs → 238μs)
- benchSequentialLookups: ~13% faster (599μs → 523μs)
- benchRandomLookups: ~16% faster (621μs → 524μs)
- benchRandomLookupsComposite: ~9% faster (2.3ms → 2.1ms)
- benchUndefinedNonexistent: ~5% faster (464μs → 440μs)
- Add normalized definitions cache with memory management
- Reorder checks to prioritize most common cases
- Add fast path for circular reference check
- Improve code readability with better comments

Performance improvements:
- benchRandomLookupsComposite: ~4% faster (2.3ms → 2.2ms)
- benchSequentialLookups: ~2% faster (544μs → 539μs)
- benchRandomLookups: ~1.5% faster (546μs → 525μs)

Memory usage is kept in check by clearing the cache when it reaches 100 entries.
- Simplify exception handling paths
- Add fast path for NotFoundException with matching ID
- Use ternary operators for cleaner code
- Improve code readability with better comments

Performance improvements:
- benchRandomLookupsComposite: ~1% faster (2.2ms → 2.18ms)
- benchSequentialLookups: ~0.5% faster (539μs → 536μs)
- benchRandomLookups: ~0.5% faster (541μs → 538μs)
- Skip parsing if already a valid definition
- Only validate meta if it's not empty
- Only unset instance if it exists
- Improve code readability with better comments

Performance improvements:
- benchConstruct: ~10% faster (238μs → 213μs)
- benchSequentialLookups: ~3.5% faster (536μs → 517μs)
- benchRandomLookups: ~3.7% faster (538μs → 518μs)
- benchRandomLookupsComposite: ~0.5% faster (2.18ms → 2.17ms)
Copy link

codecov bot commented Feb 9, 2025

Codecov Report

Attention: Patch coverage is 98.48485% with 1 line in your changes missing coverage. Please review.

Project coverage is 99.80%. Comparing base (911dbb0) to head (316cb53).

Files with missing lines Patch % Lines
src/Container.php 98.48% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##              master     #387      +/-   ##
=============================================
- Coverage     100.00%   99.80%   -0.20%     
- Complexity       169      179      +10     
=============================================
  Files             11       11              
  Lines            492      505      +13     
=============================================
+ Hits             492      504      +12     
- Misses             0        1       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@samdark samdark requested a review from a team February 9, 2025 22:09
@@ -212,12 +239,16 @@ private function addDefinition(string $id, mixed $definition): void
[$definition, $meta] = DefinitionParser::parse($definition);
if ($this->validate) {
$this->validateDefinition($definition, $id);
$this->validateMeta($meta);
// Only validate meta if it's not empty.
if ($meta !== []) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary as for me, foreach perfectly works with empty arrays

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call a method has cost.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why wouldn't you inline the whole framework in a single function?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes sense here.

}
// Fast path: check if instance exists.
if (isset($this->instances[$id])) {
return $id === StateResetter::class ? $this->prepareStateResetter($id) : $this->instances[$id];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't just a return $instances[$id]?

prepareStateResetter already holds values in the $instances, isn't it?

Copy link
Member

@vjik vjik Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prepareStateResetter checks delegates before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All delegates should be calculated and set one time, why they should do it every call?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All delegates should be calculated and set one time, why they should do it every call?

I means check delegated containters. See current code:

if ($id === StateResetter::class) {
    $delegatesResetter = null;
    if ($this->delegates->has(StateResetter::class)) {
        $delegatesResetter = $this->delegates->get(StateResetter::class);
    }

* @param string $id
* @return mixed
*/
private function prepareStateResetter(string $id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private function prepareStateResetter(string $id)
private function prepareStateResetter(): StateResetter

$id is always StateResetter::class

throw new NotFoundException($id, [$id], previous: $exception);
}
// Fast path: check if instance exists.
if (isset($this->instances[$id])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use array_key_exists instead of isset. Theoretically, result may be null.

}
throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e);
} catch (Throwable $e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems we should catch any exceptions from previous "catch" blocks also (for example, exceptions from delegates).

@@ -212,12 +239,16 @@ private function addDefinition(string $id, mixed $definition): void
[$definition, $meta] = DefinitionParser::parse($definition);
if ($this->validate) {
$this->validateDefinition($definition, $id);
$this->validateMeta($meta);
// Only validate meta if it's not empty.
if ($meta !== []) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call a method has cost.

array_map(static fn (array $data): mixed => $data[2], $methodsAndProperties),
);
// Skip validation for common simple cases.
if (is_string($definition) || $definition instanceof ContainerInterface || $definition instanceof Closure) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should validate string definitions (on empty).

Validation process is not required hard performance optimization because he is not running in production environment.

if (!$this->definitions->has($id)) {
throw new NotFoundException($id, $this->definitions->getBuildStack());
// Use cached normalized definition if available.
if (!isset($this->normalizedDefinitions[$id])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What case when needs to normalize definition not once?

@samdark samdark changed the title Perormance optimization Performance optimization Feb 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants