Оптимизация. Поиск/хранилище временных объектов
29 Sep 2014В сентябре я сменил работу. Хотел как-то об этом написать, но все некогда. Придет время, опишу подробнее куда-зачем-и-почему. Да и нельзя сейчас о многом говорить. Последние дни занимаюсь высокими нагрузками, и некоторые моменты делаешь иначе, нежели раньше. Самое интересное, к ним приходишь сам, а не с чей-то руки. Вот смотрю на себя со стороны, и вроде ничего не изменилось, а вроде и код немного другой уже, мышление. Возможно, сказывается тоска по "кодингу", я не писал "активно" почти полгода. Ну и нельзя отрицать тот факт, что мне везет с тасками. Хоть я и не мальчик для битья, но в проекте новичок и могу "позволить" себе шалости и вольности.
Позволю последнее лирическое отступление. Мне кажется, что rest api
преследует меня.
Мы измучались с ним на ProFIT (первое знакомство с rest) и как итог выступление на конференции SymfonyCamp 2012.
Изрядно улучшили архитектуру на следующем проекте, и даже можно было бы об этом рассказать вновь на конференции.
И вот на новом месте меня ждет снова rest api
, теперь предстоит работа в сторону производительности.
Сегодня я обратил внимание на один класс. Сам по себе он не представляет ничего сложного, но вызывается постоянно. Поэтому решил проверить, а нет ли чего в нем интересного. Я не стал бы писать статью, если такой кусок кода не писало большинство из читателей.
<?php
class Manager
{
private $cachedData = array();
public function add($valueObject)
{
if (!in_array($object, $cachedData)) {
$cachedData[] = $valueObject;
}
//your code
}
}
?>
По сути речь идет о внутреннем кеше. Такое мы делаем часто, когда хотим сохранить в памяти объект или сделать bulk insert. В нашем конкретном случае мы имели дело с Value Object. То есть объект у которого не изменяется состояние.
<?php
class ValueObject
{
private $a;
private $b;
private $c;
public function __construct($a, $b, $c)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
public function getA()
{
return $this->a;
}
public function getB()
{
return $this->b;
}
public function getC()
{
return $this->c;
}
}
?>
Давайте устроим скромный тест нашему коду на моем современном macbook pro. Мы 100 000 раз попробуем добавить объект. Количество возможных объектов ограничем: 8, 1000, 8000.
<?php
$startTime = microtime(true);
$collection = array();
for ($i = 0; $i < 100000; $i ++) {
$object = new ValueObject(rand(1,2), rand(1,2), rand(1,2));
//$object = new ValueObject(rand(1,10), rand(1,10), rand(1,10));
//$object = new ValueObject(rand(1,20), rand(1,20), rand(1,20));
if (!in_array($object, $collection)) {
$collection[] = $object;
}
}
echo 'Elements: ' . count($collection) . PHP_EOL;
echo memory_get_usage(true)/1048576,2 . 'Mb'. PHP_EOL;
echo microtime(true) - $startTime . PHP_EOL;
?>
Результаты следующие:
Elements: 8
0.252Mb
0.12847399711609
Elements: 1000
0.752Mb
2.2713990211487
Elements: 8000
42Mb
18.371023893356
Получается, что уже на 1000 простых объектов с 3 аттрибутами мы имеем уже приличную задержку. С ростом количества аттрибутов и самих объектов, время будет только расти.
Первое, что приходит на ум, это избавиться от сравнения объектов.
Ведь in_array
выдаст true
только при равенстве всех аттрибутов.
Напомню, у нас Value Object, и мы уверенны в неизменности его состояния. Так создадим же метод __toString
.
<?php
public function __toString()
{
return $this->a . '_' . $this->b . '_' . $this->c;
}
?>
Если вы работаете с Value Object, то настоятельно рекомендую при конвертации в строку передавать все аттрибуты.
Таким образом, вы полноправны создать обратный метод public static parse($string)
.
Спросите, а что делать с этим методом? М-м-м, на помощь нам придет мой любимый array_key_exists
. Обновим наш тест.
<?php
$startTime = microtime(true);
$collection = array();
for ($i = 0; $i < 100000; $i ++) {
$object = new ValueObject(rand(1,2), rand(1,2), rand(1,2));
//$object = new ValueObject(rand(1,10), rand(1,10), rand(1,10));
//$object = new ValueObject(rand(1,20), rand(1,20), rand(1,20));
if (!array_key_exists($string = (string) $object, $collection)) {
$collection[$string] = $object;
}
}
echo 'Elements: ' . count($collection) . PHP_EOL;
echo memory_get_usage(true)/1048576,2 . 'Mb'. PHP_EOL;
echo microtime(true) - $startTime . PHP_EOL;
?>
Результаты ниже. Стоит заметить, что "пожирание" памяти осталось таким же, а вот время исполнения скрипта "перестало" расти.
Elements: 8
0.252Mb
0.1604528427124
Elements: 1000
0.752Mb
0.15253686904907
Elements: 8000
42Mb
0.18381094932556
В нашем случае, можно пойти дальше, хранить не сам объект, а его строковое представление (не только в ключе, но и в значении).
Тогда будет решена проблема с "пожиранием" памяти. Естественно, как расплата за это, вам придется создать метод parse
и восстанавливать объект при обращении.
В любом случае, это уже зависит от частностей.
Elements: 8
0.252Mb
0.16275596618652
Elements: 1000
0.52Mb
0.16516399383545
Elements: 8000
1.752Mb
0.16776394844055
Еще занимательный факт, что если вы оставите in_array()
, но уже для строк, то код будет работать еще медленее, чем с объектами.
В заключении, хочется сказать, что объяснение лежит на поверхности и вы его легко найдете.
Так что применяйте смелее основы php core, и ваш проект будет вас радовать! Да, и ArrayCollection у doctrine не самаяя быстрая вещь ;).