Archiwum kategorii: Generatory w PHP

Duże tablice. Jak je ugryźć?

Generator

Każdy programista w swojej pracy boryka się z problemami związanymi z użyciem pamięci. Czasami urwanie 1mb jest na wagę złota i od tego może zależeć poprawne wykonanie skryptu.

Najczęstszym problemem jest procesowanie tablic zawierających tysiące rekordów. Na szczęście z pomocą przychodzi Generator. Jest to funkcja, która pozwala na iterowanie po zbiorach danych. W PHP został dodany w wersji 5.5. Aby zacząć używać funkcji Generatora wystarczy zamiast return użyć yield.

function getData()
{
	for ($i = 0; $i < 100000; $i++)
	{
		yield $i;
	}
}

$data = getData();

Funkcja getData() zwraca obiekt klasy Generator, która implementuje interface Iterator.

Dzięki temu tablica nie jest od razu budowana w pamięci, a my mamy możliwość iterowania po rekordach.

Test

Jak wygląda kwestia wydajności? Przeprowadziłem 2 testy. Jedno przy użyciu PDO i tabeli zawierającej ok 200k rekordów oraz z tablicą również zawierającą ok 200k rekordów.

<?php
class TestGenerator
{
	private $pdo;

	public function __construct()
	{
		$this->pdo = new PDO(
			'mysql:host=127.0.0.1;dbname=test;port=3300',
			'root',
			'root'
		);
	}

	public function pdoWithGenerator()
	{
		$stmt = $this->pdo->query('select id from `test`');

		while ($row = $stmt->fetch())
		{
			yield $row;
		}
		echo 'PDO Generator: ' . round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
	}

	public function pdoWithoutGenerator()
	{
		$stmt = $this->pdo->query('select id from `test`');

		$test = [];
		while ($row = $stmt->fetch())
		{
			$test[] = $row;
		}
		echo 'PDO Brak generatora: ' . round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
		return $test;
	}

	public function testArrayWithGenerator()
	{
		for ($i = 0; $i <= 200042; $i++)
		{
			yield $i;
		}
		echo 'Tablica Generator: ' . round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
	}

	public function testArrayWithoutGenerator()
	{
		$test = [];
		for ($i = 0; $i <= 200042; $i++)
		{
			$test[] = $i;
		}
		echo 'Tablica brak generatora: ' . round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
		return $test;
	}
}

$test = new TestGenerator();

foreach ($test->pdoWithGenerator() as $item){}
foreach ($test->pdoWithoutGenerator() as $item){}
foreach ($test->testArrayWithGenerator() as $item){}
foreach ($test->testArrayWithoutGenerator() as $item) {}

Podsumowanie

Wnioski z tego testu są dość oczywiste. Jak widać powyżej, używając Generatora można zaoszczędzić znaczną ilość pamięci.

Ciekawostki:
* Można wykonywać operacje po yield, np. za pomocą break.

function getData()
{
	for ($i = 0; $i < 100000; $i++)
	{
		yield $i;

		if ($i === 10)
		{
			break;
		}
	}
}

$data = getData();

foreach ($data as $i)
{
	echo $i. PHP_EOL;
}

* Można zwracać pary klucz-wartość.

function getData()
{
	for ($i = 0; $i < 10; $i++)
	{
		yield $i => $i+1;
	}
}

$data = getData();

foreach ($data as $key => $value)
{
	echo 'Key: ' . $key . PHP_EOL;
	echo 'Value: ' . $value . PHP_EOL;
}

Autorem tekstu jest Łukasz Cieślik.