PHP Performance Trivia Nikita Popov
Opcache
Opcache Method Opcodes Class Method Shared Memory
Opcache Method Opcodes Class Method Method Class Method Shared Memory Per-Request Arena copy
Opcache Method Opcodes Class Method Method Class Method Shared Memory Per-Request Arena copy Modified during inheritance
Opcache Method Opcodes Class Method Method Class Method Shared Memory Per-Request Arena copy Modified during inheritance Registered in class table
Preloading Method Opcodes Class Method Shared Memory
Preloading Method Opcodes Class Method Shared Memory MAP area Static properties Runtime cache
Preloading Method Opcodes Class Method Shared Memory MAP area Static properties Runtime cache Cleared on each request
Preloading Method Opcodes Class Method Shared Memory ● Classes must be fully inherited!
Preloading Method Opcodes Class Method Shared Memory ● Classes must be fully inherited! ● Parents/interfaces known ● All constant expressions known ● All-ish types known
Preloading Method Opcodes Class Method Shared Memory ● Classes must be fully inherited! ● Parents/interfaces known ● All constant expressions known ● All-ish types known ● Windows: lost cause (ASLR), internal classes not "known"
Preloading Method Opcodes Class Method Shared Memory ● No way to clear preload state ● Opcache reset not enough ● FPM reload not enough
Value Caching ● Only about completely static data here… – Say a composer class map
Value Caching PHP PHP PHP Opcache SHM APCU SHM
Value Caching PHP PHP PHP Opcache SHM APCU SHM Direct accessRequires copying!
Value Caching PHP PHP PHP Opcache SHM APCU SHM Direct accessRequires copying! Data never removed (*)Supports deletion
Value Caching ● Only about completely static data here… – Say a composer class map ● APCU very inefficient for large data – Requires unserialization and copying ● (Ab)use opcache as a data cache
Value Caching <?php return [ "foo" => "bar", "bar" => "baz", ]; Array stored as "immutable array" in shared memory
Opcache Reset ● Invalidated files remain in opcache ● Only cleared on full reset
Opcache Reset PHP PHP PHP Opcache SHM Wait for requests to finish
Opcache Reset PHP PHP PHP Opcache SHM Wait for requests to finish
Opcache Reset PHP PHP PHP Opcache SHM Wait for requests to finish
Opcache Reset PHP PHP PHP Opcache SHM Wait for requests to finish SIGKILL
Opcache Reset PHP PHP Opcache SHM Wait for requests to finish
Opcache Reset PHP PHP Opcache SHM Clear
Opcache Reset PHP PHP Opcache SHM
Opcache Reset ● Cache not used during reset ● Needs to be repopulated from scratch ● File cache can help mitigate
Arrays vs Objects ● Array: ["first" => $a, "second" => $b] ● Packed array: [$a, $b] ● Object with declared properties – class Test { public $first, $second; } ● Object with dynamic properties – (object)["first"=>$a, "second"=>$b]
Memory Usage 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0 100 200 300 400 500 600 700 800 Array Array (packed) Object Number of properties/keys Memoryusage(bytes)
Memory Usage 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0 100 200 300 400 500 600 700 800 Array Array (packed) Object Number of properties/keys Memoryusage(bytes) ["first" => $a, "second" => $b] [$a, $b] class Pair { public $first, $second; }
Memory Usage (Ratio) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0 1 2 3 4 5 6 7 Ratio Ratio (packed) Number of properties/keys Arraysize/objectsize
Caveat: Properties table ● Some operations materialize properties table – foreach ($object as $propName => $value) – (array) $object – var_dump($object) – …
Caveat: Properties table ● Some operations materialize properties table – foreach ($object as $propName => $value) – (array) $object – var_dump($object) – … ● You pay the price for both object and array ● No way to remove once created
Memory Usage 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0 100 200 300 400 500 600 700 800 Array Array (packed) Object Object (dyn) Number of properties/keys Memoryusage(bytes)
Garbage Collection
Garbage Collection ● Reference Counting – Count how many times a value is used – Destroy when count is zero
Garbage Collection ● Reference Counting – Count how many times a value is used – Destroy when count is zero $x = "foobar"; // refcount=1 $y = $x; // refcount=2 unset($x); // refcount=1 unset($y); // refcount=0 ==> Destroy!
Garbage Collection ● Reference Counting – Count how many times a value is used – Destroy when count is zero $x = []; // refcount=1 $x[0] =& $x; // refcount=2 unset($x); // refcount=1 // Will never reach 0 due to cycle!
Garbage Collection ● Cycle Collector – Mark & sweep algorithm
Garbage Collection ● Cycle Collector – Mark & sweep algorithm ● Start from "roots" ● Simulate what would happen if they were released ● If simulation results in refcount=0, actually destroy
Garbage Collection ● Cycle Collector – Mark & sweep algorithm – PHP <= 7.2: Fixed root buffer with 10000 entries ● 10000 objects should be enough for everyone!
Garbage Collection ● Cycle Collector – Mark & sweep algorithm – PHP <= 7.2: Fixed root buffer with 10000 entries ● 10000 objects should be enough for everyone! ● Cycle collector runs every time root buffer full ● May walk graph with millions of objects each time
Composer + GC Too many memes
Composer + GC commit ac676f47f7bbc619678a29deae097b6b0710b799 Author: Jordi Boggiano <j.boggiano@seld.be> Date: Tue Dec 2 10:23:21 2014 +0000 Disable GC when computing deps, refs #3482 diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index b76155a5..1b2a6772 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -160,6 +160,8 @@ public function __construct(... */ public function run() { + gc_disable(); + if ($this->dryRun) { $this->verbose = true; $this->runScripts = false;
Composer + GC commit ac676f47f7bbc619678a29deae097b6b0710b799 Author: Jordi Boggiano <j.boggiano@seld.be> Date: Tue Dec 2 10:23:21 2014 +0000 Disable GC when computing deps, refs #3482 diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index b76155a5..1b2a6772 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -160,6 +160,8 @@ public function __construct(... */ public function run() { + gc_disable(); + if ($this->dryRun) { $this->verbose = true; $this->runScripts = false; 2x speedup
Garbage Collection ● Cycle Collector – Mark & sweep algorithm – PHP <= 7.2: Fixed root buffer with 10000 entries – PHP >= 7.3: Dynamic root buffer ● Root buffer automatically grows ● Dynamic GC threshold ● If GC collects little garbage, GC threshold grows
Type Declarations ● Do they make PHP slower or faster?
Type Declarations ● Do they make PHP slower or faster? ● Type declarations need to be checked ● Type declarations allow more optimizations
Type Optimization <?php function powi(float $base, int $power): float { if ($power == 0) return 1.0; $result = $base; for ($i = 1; $i < $power; $i++) { $result *= $base; } return $result; }
Type Optimization <?php function powi(float $base, int $power): float { if ($power == 0) return 1.0; $result = $base; for ($i = 1; $i < $power; $i++) { $result *= $base; } return $result; } Type inference: int Type inference: float
Type Optimization <?php function powi(float $base, int $power): float { if ($power == 0) return 1.0; $result = $base; for ($i = 1; $i < $power; $i++) { $result *= $base; } return $result; } Type inference: int Type inference: float → Eliminated return type check
Type Optimization <?php function powi(float $base, int $power): float { if ($power == 0) return 1.0; $result = $base; for ($i = 1; $i < $power; $i++) { $result *= $base; } return $result; } Use specialized ZEND_PRE_INC_LONG Use specialized ZEND_MUL_DOUBLE + eliminate compound operation Use specialized ZEND_IS_SMALLER_LONG
Type Optimization <?php function powi(float $base, int $power): float { if ($power == 0) return 1.0; $result = $base; for ($i = 1; $i < $power; $i++) { $result *= $base; } return $result; } Type inference: int Type inference: float
Type Optimization <?php function powi( $base, $power) { if ($power == 0) return 1.0; $result = $base; for ($i = 1; $i < $power; $i++) { $result *= $base; } return $result; } Type inference: int|float Type inference: mixed
Type Optimization ● For this example: – Without opcache: Performance ~same with and without types – With opcache: With types 2.5x faster – Type check cost: Once – Type optimization benefit: Multiple loop iterations
Type Optimization ● For this example: – Without opcache: Performance ~same with and without types – With opcache: With types 2.5x faster – Type check cost: Once – Type optimization benefit: Multiple loop iterations ● But: Does not happen often in practice.
Global Namespace Fallback <?php namespace Foo; var_dump(strlen("foobar")); // Might be strlen() // Might be Foostrlen()
Global Namespace Fallback <?php namespace Foo; var_dump(strlen("foobar")); // Might be strlen() // Might be Foostrlen() Actual function cached on first call → Not particularly expensive
Specialized Functions ● Some functions have optimized VM instruction – strlen() and count() – is_null() etc – intval() etc – defined() – call_user_func() and call_user_func_array() – in_array() and array_key_exists() – get_class(), get_called_class() and gettype() – func_num_args() and func_get_args()
Specialized Functions ● Some functions have optimized VM instruction ● Can only be used if function known ● Requires fully qualified name or "use function"
Compile-time evaluation <?php namespace Foo; function doSomething() { if (version_compare(PHP_VERSION, '7.3', '>=')) { // PHP 7.3 implementation } else { // Fallback implementation } } Can't evaluate due to namespace fallback
Compile-time evaluation <?php namespace Foo; function doSomething() { if (version_compare(PHP_VERSION, '7.3', '>=')) { // PHP 7.3 implementation } else { // Fallback implementation } } Evaluated to true/false by opcache
Compile-time evaluation <?php namespace Foo; use function version_compare; use const PHP_VERSION; function doSomething() { if (version_compare(PHP_VERSION, '7.3', '>=')) { // PHP 7.3 implementation } else { // Fallback implementation } } Evaluated to true/false by opcache
Thank You!

PHP Performance Trivia