8000 feature #27543 [Cache] serialize objects using native arrays when pos… · Doctrs/symfony@c0ca2af · GitHub
[go: up one dir, main page]

Skip to content
/ symfony Public
forked from symfony/symfony

Commit c0ca2af

Browse files
committed
feature symfony#27543 [Cache] serialize objects using native arrays when possible (nicolas-grekas)
This PR was merged into the 4.2-dev branch. Discussion ---------- [Cache] serialize objects using native arrays when possible | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - This PR allows leveraging OPCache shared memory when storing objects in `Php*` pool storages (as done by default for all system caches). This improves performance a bit further when loading e.g. annotations, etc. (bench coming); Instead of using native php serialization, this uses a marshaller that represents objects in plain static arrays. Unmarshalling these arrays is faster than unserializing the corresponding PHP strings (because it works with copy-on-write, while unserialize cannot.) php-serialization is still a possible format because we have to use it when serializing structures with internal references or with objects implementing `Serializable`. The best serialization format is selected automatically so this is completely seamless. ping @palex-fpt since you gave me the push to work on this, and are pursuing a similar goal in symfony#27484. I'd be thrilled to get some benchmarks on your scenarios. Commits ------- 866420e [Cache] serialize objects using native arrays when possible
2 parents d075d0c + 866420e commit c0ca2af

37 files changed

+1390
-88
lines changed

.php_cs.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ return PhpCsFixer\Config::create()
2020
->append(array(__FILE__))
2121
->exclude(array(
2222
// directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code
23+
'Symfony/Component/Cache/Tests/Marshaller/Fixtures',
2324
'Symfony/Component/DependencyInjection/Tests/Fixtures',
2425
'Symfony/Component/Routing/Tests/Fixtures/dumper',
2526
// fixture templates

src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<argument /> <!-- namespace -->
4242
<argument>0</argument> <!-- default lifetime -->
4343
<argument>%kernel.cache_dir%/pools</argument>
44+
<argument>true</argument>
4445
<call method="setLogger">
4546
<argument type="service" id="logger" on-invalid="ignore" />
4647
</call>

src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,21 @@ public function get(string $key, callable $callback, float $beta = null)
8787
if (null === $this->values) {
8888
$this->initialize();
8989
}
90-
if (null === $value = $this->values[$key] ?? null) {
90+
if (!isset($this->keys[$key])) {
9191
if ($this->pool instanceof CacheInterface) {
9292
return $this->pool->get($key, $callback, $beta);
9393
}
9494

9595
return $this->doGet($this->pool, $key, $callback, $beta ?? 1.0);
9696
}
97+
$value = $this->values[$this->keys[$key]];
98+
9799
if ('N;' === $value) {
98100
return null;
99101
}
102+
if ($value instanceof \Closure) {
103+
return $value();
104+
}
100105
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
101106
return unserialize($value);
102107
}
@@ -115,15 +120,22 @@ public function getItem($key)
115120
if (null === $this->values) {
116121
$this->initialize();
117122
}
118-
if (!isset($this->values[$key])) {
123+
if (!isset($this->keys[$key])) {
119124
return $this->pool->getItem($key);
120125
}
121126

122-
$value = $this->values[$key];
127+
$value = $this->values[$this->keys[$key]];
123128
$isHit = true;
124129

125130
if ('N;' === $value) {
126131
$value = null;
132+
} elseif ($value instanceof \Closure) {
133+
try {
134+
$value = $value();
135+
} catch (\Throwable $e) {
136+
$value = null;
137+
$isHit = false;
138+
}
127139
} elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
128140
try {
129141
$value = unserialize($value);
@@ -167,7 +179,7 @@ public function hasItem($key)
167179
$this->initialize();
168180
}
169181

170-
return isset($this->values[$key]) || $this->pool->hasItem($key);
182+
return isset($this->keys[$key]) || $this->pool->hasItem($key);
171183
}
172184

173185
/**
@@ -182,7 +194,7 @@ public function deleteItem($key)
182194
$this->initialize();
183195
}
184196

185-
return !isset($this->values[$key]) && $this->pool->deleteItem($key);
197+
return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
186198
}
187199

188200
/**
@@ -198,7 +210,7 @@ public function deleteItems(array $keys)
198210
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key)));
199211
}
200212

201-
if (isset($this->values[$key])) {
213+
if (isset($this->keys[$key])) {
202214
$deleted = false;
203215
} else {
204216
$fallbackKeys[] = $key;
@@ -224,7 +236,7 @@ public function save(CacheItemInterface $item)
224236
$this->initialize();
225237
}
226238

227-
return !isset($this->values[$item->getKey()]) && $this->pool->save($item);
239+
return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
228240
}
229241

230242
/**
@@ -236,7 +248,7 @@ public function saveDeferred(CacheItemInterface $item)
236248
$this->initialize();
237249
}
238250

239-
return !isset($this->values[$item->getKey()]) && $this->pool->saveDeferred($item);
251+
return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
240252
}
241253

242254
/**
@@ -253,11 +265,17 @@ private function generateItems(array $keys): \Generator
253265
$fallbackKeys = array();
254266

255267
foreach ($keys as $key) {
256-
if (isset($this->values[$key])) {
257-
$value = $this->values[$key];
268+
if (isset($this->keys[$key])) {
269+
$value = $this->values[$this->keys[$key]];
258270

259271
if ('N;' === $value) {
260272
yield $key => $f($key, null, true);
273+
} elseif ($value instanceof \Closure) {
274+
try {
275+
yield $key => $f($key, $value(), true);
276+
} catch (\Throwable $e) {
277+
yield $key => $f($key, null, false);
278+
}
261279
} elseif (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
262280
try {
263281
yield $key => $f($key, unserialize($value), true);

src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
2020
use PhpFilesTrait;
2121

2222
/**
23+
* @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire.
24+
* Doing so is encouraged because it fits perfectly OPcache's memory model.
25+
*
2326
* @throws CacheException if OPcache is not enabled
2427
*/
25-
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null)
28+
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, bool $appendOnly = false)
2629
{
30+
$this->appendOnly = $appendOnly;
2731
self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time();
2832
parent::__construct('', $defaultLifetime);
2933
$this->init($namespace, $directory);
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Marshaller;
13+
14+
use Symfony\Component\Cache\Marshaller\PhpMarshaller\Configurator;
15+
use Symfony\Component\Cache\Marshaller\PhpMarshaller\Reference;
16+
use Symfony\Component\Cache\Marshaller\PhpMarshaller\Registry;
17+
18+
/**
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*
21+
* PhpMarshaller allows serializing PHP data structures using var_export()
22+
* while preserving all the semantics associated to serialize().
23+
*
24+
* By leveraging OPcache, the generated PHP code is faster than doing the same with unserialize().
25+
*
26+
* @internal
27+
*/
28+
class PhpMarshaller
29+
{
30+
public static function marshall($value, int &$objectsCount)
31+
{
32+
if (!\is_object($value) && !\is_array($value)) {
33+
return $value;
34+
}
35+
$objectsPool = new \SplObjectStorage();
36+
$value = array($value);
37+
$objectsCount = self::doMarshall($value, $objectsPool);
38+
39+
$classes = array();
40+
$values = array();
41+
$wakeups = array();
42+
foreach ($objectsPool as $i => $v) {
43+
list(, $classes[], $values[], $wakeup) = $objectsPool[$v];
44+
if ($wakeup) {
45+
$wakeups[$wakeup] = $i;
46+
}
47+
}
48+
ksort($wakeups);
49+
$properties = array();
50+
foreach ($values as $i => $vars) {
51+
foreach ($vars as $class => $values) {
52+
foreach ($values as $name => $v) {
53+
$properties[$class][$name][$i] = $v;
54+
}
55+
}
56+
}
57+
if (!$classes) {
58+
return $value[0];
59+
}
60+
61+
return new Configurator(new Registry($classes), $properties, $value[0], $wakeups);
62+
}
63+
64+
public static function optimize(string $exportedValue)
65+
{
66+
return preg_replace(sprintf("{%s::__set_state\(array\(\s++'0' => (\d+),\s++\)\)}", preg_quote(Reference::class)), Registry::class.'::$objects[$1]', $exportedValue);
67+
}
68+
69+
private static function doMarshall(array &$array, \SplObjectStorage $objectsPool): int
70+
{
71+
$objectsCount = 0;
72+
73+
foreach ($array as &$value) {
74+
if (\is_array($value) && $value) {
75+
$objectsCount += self::doMarshall($value, $objectsPool);
76+
}
77+
if (!\is_object($value)) {
78+
continue;
79+
}
80+
if (isset($objectsPool[$value])) {
81+
++$objectsCount;
82+
$value = new Reference($objectsPool[$value][0]);
83+
continue;
84+
}
85+
$class = \get_class($value);
86+
$properties = array();
87+
$sleep = null;
88+
$arrayValue = (array) $value;
89+
$proto = (Registry::$reflectors[$class] ?? Registry::getClassReflector($class))->newInstanceWithoutConstructor();
90+
91+
if ($value instanceof \ArrayIterator || $value instanceof \ArrayObject) {
92+
// ArrayIterator and ArrayObject need special care because their "flags"
93+
// option changes the behavior of the (array) casting operator.
94+
$reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject';
95+
$reflector = Registry::$reflectors[$reflector] ?? Registry::getClassReflector($reflector);
96+
97+
$properties = array(
98+
$arrayValue,
99+
$reflector->getMethod('getFlags')->invoke($value),
100+
$value instanceof \ArrayObject ? $reflector->getMethod('getIteratorClass')->invoke($value) : 'ArrayIterator',
101+
);
102+
103+
$reflector = $reflector->getMethod('setFlags');
104+
$reflector->invoke($proto, \ArrayObject::STD_PROP_LIST);
105+
106+
if ($properties[1] & \ArrayObject::STD_PROP_LIST) {
107+
$reflector->invoke($value, 0);
108+
$properties[0] = (array) $value;
109+
} else {
110+
$reflector->invoke($value, \ArrayObject::STD_PROP_LIST);
111+
$arrayValue = (array) $value;
112+
}
113+
$reflector->invoke($value, $properties[1]);
114+
115+
if (array(array(), 0, 'ArrayIterator') === $properties) {
116+
$properties = array();
117+
} else {
118+
if ('ArrayIterator' === $properties[2]) {
119+
unset($properties[2]);
120+
}
121+
$properties = array($reflector->class => array("\0" => $properties));
122+
}
123+
} elseif ($value instanceof \SplObjectStorage) {
124+
foreach (clone $value as $v) {
125+
$properties[] = $v;
126+
$properties[] = $value[$v];
127+
}
128+
$properties = array('SplObjectStorage' => array("\0" => $properties));
129+
} elseif ($value instanceof \Serializable) {
130+
++$objectsCount;
131+
$objectsPool[$value] = array($id = \count($objectsPool), serialize($value), array(), 0);
132+
$value = new Reference($id);
133+
continue;
134+
}
135+
136+
if (\method_exists($class, '__sleep')) {
137+
if (!\is_array($sleep = $value->__sleep())) {
138+
trigger_error('serialize(): __sleep should return an array only containing the names of instance-variables to serialize', E_USER_NOTICE);
139+
$value = null;
140+
continue;
141+
}
142+
$sleep = array_flip($sleep);
143+
}
144+
145+
$proto = (array) $proto;
146+
147+
foreach ($arrayValue as $name => $v) {
148+
$k = (string) $name;
149+
if ('' === $k || "\0" !== $k[0]) {
150+
$c = $class;
151+
} elseif ('*' === $k[1]) {
152+
$c = $class;
153+
$k = substr($k, 3);
154+
} else {
155+
$i = strpos($k, "\0", 2);
156+
$c = substr($k, 1, $i - 1);
157+
$k = substr($k, 1 + $i);
158+
}
159+
if (null === $sleep) {
160+
$properties[$c][$k] = $v;
161+
} elseif (isset($sleep[$k]) && $c === $class) {
162+
$properties[$c][$k] = $v;
163+
unset($sleep[$k]);
164+
}
165+
if (\array_key_exists($name, $proto) && $proto[$name] === $v) {
166+
unset($properties[$c][$k]);
167+
}
168+
}
169+
if ($sleep) {
170+
foreach ($sleep as $k => $v) {
171+
trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $k), E_USER_NOTICE);
172+
}
173+
}
174+
175+
$objectsPool[$value] = array($id = \count($objectsPool));
176+
$objectsCount += 1 + self::doMarshall($properties, $objectsPool);
177+
$objectsPool[$value] = array($id, $class, $properties, \method_exists($class, '__wakeup') ? $objectsCount : 0);
178+
179+
$value = new Reference($id);
180+
}
181+
182+
return $objectsCount;
183+
}
184+
}

0 commit comments

Comments
 (0)
0