Статьи

Повышение производительности памяти с генераторами и Nikic / Iter

Массивы и, как следствие, итерации, являются фундаментальными частями любого приложения. Как и сложность наших приложений, то, как мы их используем, должно развиваться по мере того, как мы получаем доступ к новым инструментам.

Новые инструменты, например, генераторы. Сначала появились массивы. Затем мы получили возможность определять наши собственные вещи, похожие на массивы (называемые итераторами). Но начиная с PHP 5.5 мы можем быстро создавать итератороподобные структуры, называемые генераторами.

A loop illustration

Они появляются как функции, но мы можем использовать их как итераторы. Они дают нам простой синтаксис для того, что по сути является прерываемой, повторяемой функцией. Они замечательные!

И мы рассмотрим несколько областей, в которых мы можем их использовать. Мы также собираемся обнаружить несколько проблем, о которых нужно знать при их использовании. Наконец, мы изучим блестящую библиотеку, созданную талантливым Никитой Поповым .

Вы можете найти пример кода по адресу https://github.com/sitepoint-editors/generators-and-iter .

Проблемы

Представьте, что у вас много реляционных данных, и вы хотите загружать их. Возможно, данные разделены запятыми, и вам нужно загрузить каждый тип данных и связать их вместе.

Вы можете начать с чего-то простого:

function readCSV($file) {
    $rows = [];

    $handle = fopen($file, "r");

    while (!feof($handle)) {
        $rows[] = fgetcsv($handle);
    }

    fclose($handle);

    return $rows;
}

$authors = array_filter(
    readCSV("authors.csv")
);

$categories = array_filter(
    readCSV("categories.csv")
);

$posts = array_filter(
    readCSV("posts.csv")
);

Тогда вы, вероятно, попытаетесь соединить связанные элементы с помощью итераций или функций более высокого порядка:

 function filterByColumn($array, $column, $value) {
    return array_filter(
        $array, function($item) use ($column, $value) {
            return $item[$column] == $value;
        }
    );
}

$authors = array_map(function($author) use ($posts) {
    $author["posts"] = filterByColumn(
        $posts, 1, $author[0]
    );

    // make other changes to $author

    return $author;
}, $authors);

$categories = array_map(function($category) use ($posts) {
    $category["posts"] = filterByColumn(
        $posts, 2, $category[0]
    );

    // make other changes to $category

    return $category;
}, $categories);

$posts = array_map(function($post) use ($authors, $categories) {
    foreach ($authors as $author) {
        if ($author[0] == $post[1]) {
            $post["author"] = $author;
            break;
        }
    }

    foreach ($categories as $category) {
        if ($category[0] == $post[1]) {
            $post["category"] = $category;
            break;
        }
    }

    // make other changes to $post

    return $post;
}, $posts);

Кажется хорошо, верно? Ну, что происходит, когда у нас есть огромные файлы CSV для анализа? Давайте немного профилируем использование памяти…

 function formatBytes($bytes, $precision = 2) {
    $kilobyte = 1024;
    $megabyte = 1024 * 1024;

    if ($bytes >= 0 && $bytes < $kilobyte) {
        return $bytes . " b";
    }

    if ($bytes >= $kilobyte && $bytes < $megabyte) {
        return round($bytes / $kilobyte, $precision) . " kb";
    }

    return round($bytes / $megabyte, $precision) . " mb";
}

print "memory:" . formatBytes(memory_get_peak_usage());

Пример кода включает generate.php

Если у вас есть большие файлы CSV, этот код должен показать, сколько памяти требуется, чтобы связать эти массивы вместе. Это по крайней мере размер файла, который вы должны прочитать, потому что PHP должен хранить все это в памяти.

Генераторы на помощь!

Один из способов улучшить это — использовать генераторы. Если вы не знакомы с ними, сейчас самое время узнать больше .

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

 function readCSVGenerator($file) {
    $handle = fopen($file, "r");

    while (!feof($handle)) {
        yield fgetcsv($handle);
    }

    fclose($handle);
}

Если вы зациклите данные CSV, вы сразу заметите, что объем памяти вам необходим:

 foreach (readCSVGenerator("posts.csv") as $post) {
    // do something with $post
}

print "memory:" . formatBytes(memory_get_peak_usage());

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

Для начала, array_filterarray_map Вам нужно будет найти другие инструменты для обработки таких данных. Вот тот, который вы можете попробовать!

 composer require nikic/iter

Эта библиотека представляет несколько функций, которые работают с итераторами и генераторами. Так как же вы все еще можете получать все эти важные данные, не сохраняя их в памяти?

 function getAuthors() {
    $authors = readCSVGenerator("authors.csv");

    foreach ($authors as $author) {
        yield formatAuthor($author);
    }
}

function formatAuthor($author) {
    $author["posts"] = getPostsForAuthor($author);

    // make other changes to $author

    return $author;
}

function getPostsForAuthor($author) {
    $posts = readCSVGenerator("posts.csv");

    foreach ($posts as $post) {
        if ($post[1] == $author[0]) {
            yield formatPost($post);
        }
    }
}

function formatPost($post) {
    foreach (getAuthors() as $author) {
        if ($post[1] == $author[0]) {
            $post["author"] = $author;
            break;
        }
    }

    foreach (getCategories() as $category) {
        if ($post[2] == $category[0]) {
            $post["category"] = $category;
            break;
        }
    }

    // make other changes to $post

    return $post;
}

function getCategories() {
    $categories = readCSVGenerator("categories.csv");

    foreach ($categories as $category) {
        yield formatCategory($category);
    }
}

function formatCategory($category) {
    $category["posts"] = getPostsForCategory($category);

    // make other changes to $category

    return $category;
}

function getPostsForCategory($category) {
    $posts = readCSVGenerator("posts.csv");

    foreach ($posts as $post) {
        if ($post[2] == $category[0]) {
            yield formatPost($post);
        }
    }
}

// testing this out...

foreach (getAuthors() as $author) {
    foreach ($author["posts"] as $post) {
        var_dump($post["author"]);
        break 2;
    }
}

Это может быть менее многословно:

 function filterGenerator($generator, $column, $value) {
    return iter\filter(
        function($item) use ($column, $value) {
            return $item[$column] == $value;
        },
        $generator
    );
}

function getAuthors() {
    return iter\map(
        "formatAuthor",
        readCSVGenerator("authors.csv")
    );
}

function formatAuthor($author) {
    $author["posts"] = getPostsForAuthor($author);

    // make other changes to $author

    return $author;
}

function getPostsForAuthor($author) {
    return iter\map(
        "formatPost",
        filterGenerator(
            readCSVGenerator("posts.csv"), 1, $author[0]
        )
    );
}

function formatPost($post) {
    foreach (getAuthors() as $author) {
        if ($post[1] == $author[0]) {
            $post["author"] = $author;
            break;
        }
    }

    foreach (getCategories() as $category) {
        if ($post[2] == $category[0]) {
            $post["category"] = $category;
            break;
        }
    }

    // make other changes to $post

    return $post;
}

function getCategories() {
    return iter\map(
        "formatCategory",
        readCSVGenerator("categories.csv")
    );
}

function formatCategory($category) {
    $category["posts"] = getPostsForCategory($category);

    // make other changes to $category

    return $category;
}

function getPostsForCategory($category) {
    return iter\map(
        "formatPost",
        filterGenerator(
            readCSVGenerator("posts.csv"), 2, $category[0]
        )
    );
}

Немного расточительно каждый раз перечитывать каждый источник данных. Подумайте о том, чтобы хранить в памяти меньшие связанные данные (например, авторов и категории)

Другие забавные вещи

Это только верхушка айсберга, когда дело доходит до библиотеки Никича! Всегда хотел сгладить массив (или итератор / генератор)?

 $array = iter\toArray(
    iter\flatten(
        [1, 2, [3, 4, 5], 6, 7]
    )
);

print join(", ", $array); // "1, 2, 3, 4, 5"

Вы можете возвращать фрагменты итерируемых переменных, используя такие функции, как slicetake

 $array = iter\toArray(
    iter\slice(
        [-3, -2, -1, 0, 1, 2, 3],
        2, 4
    )
);

print join(", ", $array); // "-1, 0, 1, 2"

Поскольку вы больше работаете с генераторами, вы можете обнаружить, что не всегда можете их повторно использовать. Рассмотрим следующий пример:

 $mapper = iter\map(
    function($item) {
        return $item * 2;
    },
    [1, 2, 3]
);

print join(", ", iter\toArray($mapper));
print join(", ", iter\toArray($mapper));

Если вы попытаетесь запустить этот код, вы увидите исключение, говорящее; «Невозможно пройти через уже закрытый генератор». Каждая функция итератора в этой библиотеке имеет перезаписываемый аналог:

 $mapper = iter\rewindable\map(
    function($item) {
        return $item * 2;
    },
    [1, 2, 3]
);

Вы можете использовать эту функцию отображения много раз. Вы можете даже сделать свои собственные генераторы перематываемыми:

 $rewindable = iter\makeRewindable(function($max = 13) {
    $older = 0;
    $newer = 1;

    do {
        $number = $newer + $older;

        $older = $newer;
        $newer = $number;

        yield $number;
    }
    while($number < $max);
});

print join(", ", iter\toArray($rewindable()));

Что вы получаете от этого, это генератор многоразового использования!

Вывод

Для каждой цикличной вещи, о которой вам нужно подумать, генераторы могут быть опцией. Они могут быть полезны и для других вещей . И там, где язык терпит неудачу, библиотека Никича вступает с множеством функций более высокого порядка.

Вы уже используете генераторы? Хотели бы вы увидеть больше примеров того, как реализовать их в своих собственных приложениях, чтобы повысить производительность? Дайте нам знать!