Оптимизация. Поиск/хранилище временных объектов

В сентябре я сменил работу. Хотел как-то об этом написать, но все некогда. Придет время, опишу подробнее куда-зачем-и-почему. Да и нельзя сейчас о многом говорить. Последние дни занимаюсь высокими нагрузками, и некоторые моменты делаешь иначе, нежели раньше. Самое интересное, к ним приходишь сам, а не с чей-то руки. Вот смотрю на себя со стороны, и вроде ничего не изменилось, а вроде и код немного другой уже, мышление. Возможно, сказывается тоска по "кодингу", я не писал "активно" почти полгода. Ну и нельзя отрицать тот факт, что мне везет с тасками. Хоть я и не мальчик для битья, но в проекте новичок и могу "позволить" себе шалости и вольности.

Позволю последнее лирическое отступление. Мне кажется, что 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 не самаяя быстрая вещь ;).

Комментарии