PHP Profi

SPL итераторы вопреки производительности Перевод

 

Итераторы SPL вопреки производительности

Эта тема на некоторое время застряла у меня в памяти. Вдохновлённый презентацией Joshua Thijssen’s php в Великобритании о (пере)открытии SPL в php, я решил исследовать эту тему более тщательно. Я должен признать, что мне потребовалось некоторое время, чтобы понять, как все это работает и как избежать неправильного толкования назначения каждого оператора из-за отсутствия документации. Я сделал пару ошибок и возможно сделаю еще, но, как сказал Joshua в своём выступлении, документация по spl совершенно бесполезна.

Сегодня я хотел бы сосредоточиться только на одной части документации - итераторах, потому что достаточно заманчиво решить проблему с их использованием. Главное преимущество итераторов php -  простой способ для достижения повторного использования, единообразие и симпатичная архитектура с паттернами проектирования. Я хотел бы показать,  как быстро некоторые из общих проблем могут быть решены с помощью операторов и после проверить, влияет ли это как-то на производительность.

 

Шаблоны проектирования

Подключение итераторов - Decorator


Шаблон Декоратор позволяет добавлять поведение для отдельного объекта не затрагивая поведение других объектов из того же класса. В качестве примера мы можем рассмотреть расчет цены для оплаты в интернет-магазине, где вам нужно добавить некоторые налоги, цену доставки или пошлину до окончательного формирования цены.

Untitled DiagramUntitled Diagram

Разделяя наши требования  создадим отдельные классы для каждого вида расчета премии. Три части объекта будут решать, какие расчеты должны быть включены в конечную цену. Код будет выглядеть следующим образом:

$price = new RegularPrice(120);
$price = new TaxPrice($price);
$price = new ShipmentPrice($price);
if ($dutyRequired) {
    $price = new DutyPrice($price);
}

echo $price->getPrice();


Предыдущий объект передается в качестве аргумента в следующий. Когда вызывается метод getprice, будет вызван такой же метод из переданного объекта. В результате у нас есть одна рассчитаная цена для всех объектов.
Итератор работает так же, как в этом случае. Мы создаем главный объект, который называется ArrayIterator и добавляем пользовательские фильтры, итераторы, парсеры  и т.д.

$arrayIterator = new ArrayIterator(['apple', 'orange', 'beans', 'pumpkin', 'onion']);
$arrayIterator = new RandomizeArrayIterator($arrayIterator);
$arrayIterator = new FruitIterator($arrayIterator);
$arrayIterator = new LimitIterator($arrayIterator, 0, 3);

 

FilterIterator

Как видите это не так трудно, создать объекты, отвечающие только за одну вещь. На мой взгляд, один из самых полезных итераторов -   FilterIterator, от которого унаследован FruitIterator из примера. Код может выглядеть примерно так:

class FruitIterator extends FilterIterator
{
    protected $acceptedFruits = ['apple', 'orange'];

    public function accept()
    {
        return in_array($this->getInnerIterator()->current(), $this->acceptedFruits);
    }
}


Всё необходимое помещается в метод accept(), который возвращает булево значение, может ли запись быть отобрана. С другой стороны, мы не должны реализовывать новый класс каждый раз. Мы можем использовать CallbackFilterIterator и просто добавить замыкания.

$fruits = new CallbackFilterIterator($arrayIterator, function ($current, $key, $iterator) use ($acceptedFruits) {
    return in_array($current, $acceptedFruits)
});

 

Шаблон Aggregate

Этот шаблон относится к понятиям статистических данных или программированию. Оба использования подразумевают что-то большое, как совокупность из более простых, мелких составляющих. Это скорее относится к объекту, например, список, вектор, генератор, который обеспечивает интерфейс для создания итераторов.

Итак, создадим класс 'Salad', который будет содержать ингредиенты из предыдущего случая. Стандартный код будет выглядеть так:

class Salad
{
    protected $ingredients;

    public function __construct(array $ingredients)
    {
        $this->ingredients = $ingredients;
    }

    public function getIngredients()
    {
        return $this->ingredients;
    }
}

$greekSalad = new Salad(['Romaine lettuce', 'Onion', 'Black Olives', 'Green bell pepper', 'Red bell pepper', 'Tomatoes', 'Cucumber', 'Feta cheese', 'Olive Oil', 'Oregano', 'Lemon']);

foreach ($greekSalad->getIngredients() as $ingredient) {
    echo $ingredient;
}


Но мы можем использовать стандартную компоненту из библиотеки SPL в качестве IteratorAggregate, и наш код немного изменится:

class Salad implements IteratorAggregate
{
    protected $ingredients;

    public function __construct(array $ingredients)
    {
        $this--->ingredients = $ingredients;
    }

    function getIterator()
    {
        return new ArrayIterator($this->ingredients);
    }
}

$greekSalad = new Salad(['Romaine lettuce', 'Onion', 'Black Olives', 'Green bell pepper', 'Red bell pepper', 'Tomatoes', 'Cucumber', 'Feta cheese', 'Olive Oil', 'Oregano', 'Lemon']);

foreach ($greekSalad as $ingredient) {
    echo $ingredient;
}


Это может быть очень полезно, но только когда классы небольшие,  так что не забывайте о принципах SOLID.

 

Производительность

До сих пор все выглядит многообещающе. Два примера выше показывают нам, как изменить корявый foreach или циклы в объектно-ориентированной архитектуре. Но главный вопрос - действительно ли так будет умнее? Я проверил, результат меня удивил.

Я создал две части кода, чтобы проверить, какой метод быстрее - с помощью полноценного объекта итерации с SPL или с помощью убогих циклов.

$array = array();
for ($i = 0; $i < $argv[1]; $i++) {
    $array[] = rand(0, 100);
}

foreach ($array as $i => $a) {
    if (0 == $a % 2) {
        unset($array[$i]);
    }
}

return $array;

foreach ($array as $a) {}
class RandArrayIterator extends ArrayIterator
{
    public function current()
    {
        return rand(0, 100);
    }
}

class OddIterator extends FilterIterator
{
    public function accept()
    {
        return 0 == $this->getInnerIterator()->current()%2;
    }
}

$arrayIterator = new ArrayIterator(['']);
$arrayIterator = new RandArrayIterator($arrayIterator);
$arrayIterator = new InfiniteIterator($arrayIterator);
$arrayIterator = new LimitIterator($arrayIterator, 0, $argv[1]);
$arrayIterator = new OddIterator($arrayIterator);

foreach ($arrayIterator as $a){}.

Оба делают одно и то же. Создание большого массива со случайной выборкой только нечетных чисел. Я запустил их на 1000000 записей и время выполнения меня удивило. Я не был готов к такой разнице:

Циклы: 1,42 s

SPL: 7,82 s

Как бы красивы не были итераторы в SPL, они не настолько быстры, как стандартный цикл. Разница настолько велика, что теперь я буду осторожен с применением итератора.
 

2015-04-27 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий