You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
minor #61239 [OptionsResolver] Optimize splitOutsideParenthesis() - 5.9x faster (bendavies)
This PR was squashed before being merged into the 7.4 branch.
Discussion
----------
[OptionsResolver] Optimize splitOutsideParenthesis() - 5.9x faster
| Q | A
| ------------- | ---
| Branch? | 7.4
| Bug fix? | no
| New feature? | no <!-- if yes, also update src/**/CHANGELOG.md -->
| Deprecations? | no <!-- if yes, also update UPGRADE-*.md and src/**/CHANGELOG.md -->
| Issues | Fix#59354 <!-- prefix each issue number with "Fix #"; no need to create an issue if none exists, explain below -->
| License | MIT
This PR optimises the `splitOutsideParenthesis` method in `OptionsResolver.php`, achieving a 2.91x performance improvement.
I discovered this method as a performance hotspot while benchmarking a large Symfony form with many fields. Profiling revealed that `splitOutsideParenthesis` was consuming a significant portion of the form processing time.
The `splitOutsideParenthesis` method (introduced in [PR #59354](#59354)) is called frequently during options resolution and has several performance bottlenecks:
1. Character-by-character string concatenation creates new string objects on each iteration (particularly inefficient in PHP due to copy-on-write behavior)
2. All input strings are processed the same way, regardless of complexity - no fast path for simple types
3. Multiple conditional checks per character
## Test Methodology
Here's how all performance measurements were conducted:
- **Benchmark tool**: hyperfine (10 runs with 1 warmup run)
- **Test iterations**: 100,000 iterations per test case
- **Test data**: 16 different type patterns:
- Simple types: `string`, `int`, `bool`, `array`
- Union types: `string|int`, `string|int|bool`, `string|int|bool|array`
- Parentheses types: `string|(int|bool)`, `(string|int)|bool`
- Nested types: `string|(int|(bool|float))`, `(string|int)|(bool|float)`
- Array types: `string[]`, `int[]`
- Class types: `MyClass`, `\\Namespace\\Class`
- Complex union: `string|int|bool|array|object|resource|callable`
Each optimisation was tested in isolation to measure its individual impact, then all optimisations were combined for the final benchmark.
## Optimisations
### 1. Fast Path for Simple Types (No Pipes)
Most type declarations are simple types like `string`, `int`, `MyClass`, etc. without any union types.
**Implementation:**
```php
if (!\str_contains($type, '|')) {
return [$type];
}
```
### 2. Fast Path for Union Types (No Parentheses)
Common union types like `string|int|bool` don't need complex parsing - PHP's `explode()` is much faster.
**Implementation:**
```php
if (!\str_contains($type, '(') && !\str_contains($type, ')')) {
return \explode('|', $type);
}
```
### 3. Eliminate String Concatenation
String concatenation in loops creates memory overhead. Using `substr()` avoids creating intermediate strings.
**Implementation:**
```php
// Instead of: $currentPart .= $char;
// Use: $parts[] = \substr($type, $start, $i - $start);
```
### 4. Switch Statement Optimisation
Eliminates Multiple conditional checks per character.
**Implementation:**
```php
switch ($char) {
case '(':
++$parenthesisLevel;
break;
case ')':
--$parenthesisLevel;
break;
case '|':
// ...
}
```
## Benchmark Results
### Individual Optimisation Impact
Testing each optimisation in isolation:
```bash
hyperfine --warmup 1 --runs 10 \
--sort=command \
--reference 'php test_original.php' \
'php test_opt1_fast_path_simple.php' \
'php test_opt2_fast_path_union.php' \
'php test_opt3_no_string_concat.php' \
'php test_opt4_switch_statement.php'
```
```
Relative speed comparison
1.00 php test_original.php
1.23 ± 0.02 php test_opt1_fast_path_simple.php
1.95 ± 0.04 php test_opt2_fast_path_union.php
1.13 ± 0.03 php test_opt3_no_string_concat.php
1.35 ± 0.03 php test_opt4_switch_statement.php
```
### Combined Optimisation Impact
Combining all optimisations:
```bash
hyperfine --warmup 1 --runs 10 \
--sort=command \
--reference 'php test_original.php' \
'php test_optimised.php'
```
```
Relative speed comparison
1.00 php test_original.php
2.91 ± 0.03 php test_optimised.php
```
Commits
-------
b568fef [OptionsResolver] Optimize splitOutsideParenthesis() - 5.9x faster
['string|(int|bool)', [], 'The option "option" with value array is expected to be of type "string|(int|bool)", but is of type "array".'],
2155
+
['string|(int|bool)', $object, 'The option "option" with value stdClass is expected to be of type "string|(int|bool)", but is of type "stdClass".'],
2156
+
['string|(int|bool)', $resource, 'The option "option" with value resource is expected to be of type "string|(int|bool)", but is of type "resource (stream)".'],
2157
+
['string|(int|bool)', null, 'The option "option" with value null is expected to be of type "string|(int|bool)", but is of type "null".'],
2158
+
['string|(int|bool)', 3.14, 'The option "option" with value 3.14 is expected to be of type "string|(int|bool)", but is of type "float".'],
2159
+
2160
+
// Test 2 levels of nesting
2161
+
['string|(int|(bool|float))', [], 'The option "option" with value array is expected to be of type "string|(int|(bool|float))", but is of type "array".'],
2162
+
['string|(int|(bool|float))', $object, 'The option "option" with value stdClass is expected to be of type "string|(int|(bool|float))", but is of type "stdClass".'],
2163
+
['string|(int|(bool|float))', $resource, 'The option "option" with value resource is expected to be of type "string|(int|(bool|float))", but is of type "resource (stream)".'],
2164
+
['string|(int|(bool|float))', null, 'The option "option" with value null is expected to be of type "string|(int|(bool|float))", but is of type "null".'],
2165
+
2166
+
// Test 3 levels of nesting
2167
+
['string|(int|(bool|(float|null)))', [], 'The option "option" with value array is expected to be of type "string|(int|(bool|(float|null)))", but is of type "array".'],
2168
+
['string|(int|(bool|(float|null)))', $object, 'The option "option" with value stdClass is expected to be of type "string|(int|(bool|(float|null)))", but is of type "stdClass".'],
2169
+
['string|(int|(bool|(float|null)))', $resource, 'The option "option" with value resource is expected to be of type "string|(int|(bool|(float|null)))", but is of type "resource (stream)".'],
2170
+
2171
+
// Test arrays with nested union types
2172
+
['(string|int)[]|(bool|float)[]', ['test', true], 'The option "option" with value array is expected to be of type "(string|int)[]|(bool|float)[]", but one of the elements is of type "array".'],
2173
+
['(string|int)[]|(bool|float)[]', [42, 3.14], 'The option "option" with value array is expected to be of type "(string|int)[]|(bool|float)[]", but one of the elements is of type "array".'],
2174
+
2175
+
// Test deeply nested arrays with unions
2176
+
['((string|int)|(bool|float))[]', 'test', 'The option "option" with value "test" is expected to be of type "((string|int)|(bool|float))[]", but is of type "string".'],
2177
+
['((string|int)|(bool|float))[]', [null], 'The option "option" with value array is expected to be of type "((string|int)|(bool|float))[]", but one of the elements is of type "null".'],
2178
+
['((string|int)|(bool|float))[]', [$object], 'The option "option" with value array is expected to be of type "((string|int)|(bool|float))[]", but one of the elements is of type "stdClass".'],
2179
+
2180
+
// Test complex nested array types
2181
+
['(string|(int|bool)[])|(float|(null|object)[])', ['test'], 'The option "option" with value array is expected to be of type "(string|(int|bool)[])|(float|(null|object)[])", but is of type "array".'],
2182
+
['(string|(int|bool)[])|(float|(null|object)[])', [3.14], 'The option "option" with value array is expected to be of type "(string|(int|bool)[])|(float|(null|object)[])", but is of type "array".'],
0 commit comments