Co oznacza wydajność w PHP?


232

Niedawno natknąłem się na ten kod:

function xrange($min, $max) 
{
    for ($i = $min; $i <= $max; $i++) {
        yield $i;
    }
}

Nigdy wcześniej nie widziałem tego yieldsłowa kluczowego. Próbuję uruchomić kod

Błąd analizy: błąd składni, nieoczekiwany T_VARIABLE w linii x

Czym jest to yieldsłowo kluczowe? Czy to w ogóle PHP? A jeśli tak, to jak go używać?

Odpowiedzi:


355

Co to jest yield?

Do yieldkluczowych danych wraca z funkcji generującej:

Sercem funkcji generatora jest słowo kluczowe wydajności. W najprostszej formie instrukcja return wygląda bardzo podobnie do instrukcji return, z tą różnicą, że zamiast zatrzymywać wykonywanie funkcji i zwracać, wydajność zamiast tego podaje wartość pętli kodu nad generatorem i wstrzymuje wykonywanie funkcji generatora.

Co to jest funkcja generatora?

Funkcja generatora jest bardziej kompaktowym i wydajnym sposobem na napisanie Iteratora . To pozwala zdefiniować funkcję (twój xrange), które będą obliczenia i zwrotu wartości podczas jesteś zapętlenie nad nim :

foreach (xrange(1, 10) as $key => $value) {
    echo "$key => $value", PHP_EOL;
}

Spowoduje to utworzenie następującego wyniku:

0 => 1
1 => 2

9 => 10

Można również kontrolować $keyw foreachza pomocą

yield $someKey => $someValue;

W funkcji generatora $someKeyjest to, czego szukasz $keyi $someValuektóra ma być wartością $val. W przykładzie pytania to jest $i.

Jaka jest różnica w stosunku do normalnych funkcji?

Teraz możesz się zastanawiać, dlaczego nie używamy po prostu natywnej rangefunkcji PHP do osiągnięcia tego wyniku. I masz rację. Wynik byłby taki sam. Różnica polega na tym, jak się tam dostaliśmy.

Gdy używamy rangePHP, będzie go wykonać, należy utworzyć całą tablicę liczb w pamięci i returnże cała tablica do foreachpętli, która będzie następnie przejść nad nim i wyjście wartości. Innymi słowy, foreachbędzie działać na samej tablicy. rangeFunkcja i foreachtylko „rozmowa” raz. Pomyśl o tym jak o otrzymaniu paczki pocztą. Doręczyciel wręczy ci paczkę i wyjdzie. A następnie rozpakowujesz całą paczkę, wyjmując wszystko, co jest w środku.

Kiedy użyjemy funkcji generatora, PHP wejdzie do funkcji i wykona ją, dopóki nie napotka końca lub yieldsłowa kluczowego. Po napotkaniu znaku a yield, zwróci dowolną wartość w tym czasie do zewnętrznej pętli. Następnie wraca do funkcji generatora i kontynuuje od miejsca, w którym ustąpił. Ponieważ Twoja pętla xrangezawiera forpętlę, będzie ona wykonywana i ustępowała, dopóki nie $maxzostanie osiągnięta. Pomyśl o tym jak foreacho generatorze grającym w ping ponga.

Dlaczego tego potrzebuję?

Oczywiście, generatory mogą być używane do obejścia limitów pamięci. W zależności od środowiska robienie range(1, 1000000)skryptu zakończy się śmiercią, podczas gdy to samo z generatorem będzie działać dobrze. Lub, jak ujęła to Wikipedia:

Ponieważ generatory obliczają uzyskane wartości tylko na żądanie, są przydatne do reprezentowania sekwencji, które byłyby drogie lub niemożliwe do obliczenia na raz. Należą do nich np. Sekwencje nieskończone i strumienie danych na żywo.

Generatory również powinny być dość szybkie. Pamiętaj jednak, że kiedy mówimy o szybkim czasie, zwykle rozmawiamy bardzo małymi liczbami. Zanim więc wybiegniesz i zmienisz cały kod na generatory, wykonaj test porównawczy, aby zobaczyć, gdzie ma to sens.

Innym przypadkiem użycia generatorów są asynchroniczne coroutines. Słowo yieldkluczowe nie tylko zwraca wartości, ale także je akceptuje. Aby uzyskać szczegółowe informacje na ten temat, zobacz dwa doskonałe posty na blogu połączone poniżej.

Od kiedy mogę używać yield?

Generatory zostały wprowadzone w PHP 5.5 . Próba użycia yieldprzed tą wersją spowoduje różne błędy analizy, w zależności od kodu występującego po słowie kluczowym. Jeśli więc wystąpi błąd analizy składni z tego kodu, zaktualizuj PHP.

Źródła i dalsze czytanie:


1
Proszę wyjaśnić, na czym polegają korzyści yeild, powiedzmy, takie rozwiązanie: ideone.com/xgqevM
Mike

1
Ach, cóż, i generowane przeze mnie powiadomienia. Huh Cóż, eksperymentowałem z emulowaniem Generatorów dla PHP> = 5.0.0 z klasą pomocniczą i tak, nieco mniej czytelna, ale mogę tego użyć w przyszłości. Interesujący temat. Dzięki!
Mike

Nie czytelność, ale użycie pamięci! Porównaj używaną pamięć do iteracji return range(1,100000000)i for ($i=0; $i<100000000; $i++) yield $i
emix

@ Mike tak, to już wyjaśniono w mojej odpowiedzi. W drugim przykładzie Mike nie stanowi problemu, ponieważ iteruje tylko 10 wartości.
Gordon

1
@Mike Jednym z problemów związanych z Xrange jest to, że użycie limitów statyki jest przydatne na przykład do zagnieżdżania (na przykład przeszukiwanie kolektora n-wymiarowego lub rekursywnego szybkiego sortowania za pomocą generatorów). Nie można zagnieżdżać pętli xrange, ponieważ istnieje tylko jedna instancja licznika. Wersja Yield nie ma tego problemu.
Shayne,

43

Ta funkcja używa wydajności:

function a($items) {
    foreach ($items as $item) {
        yield $item + 1;
    }
}

jest prawie taki sam jak ten bez:

function b($items) {
    $result = [];
    foreach ($items as $item) {
        $result[] = $item + 1;
    }
    return $result;
}

Jedyną różnicą jest to, że a()zwraca generator i b()tylko prostą tablicę. Możesz iterować w obu przypadkach.

Ponadto, pierwszy nie przydziela pełnej tablicy, a zatem wymaga mniej pamięci.


2
dodaj uwagi z oficjalnych dokumentów: W PHP 5 generator nie mógł zwrócić wartości: spowodowałoby to błąd kompilacji. Pusta instrukcja return była poprawną składnią w generatorze i kończyła generator. Od PHP 7.0 generator może zwracać wartości, które można odzyskać za pomocą Generator :: getReturn (). php.net/manual/en/language.generators.syntax.php
Programista Dancuk

Prosty i zwięzły.
John Miller,

24

prosty przykład

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $v)
    echo $v.',';
echo '#end main#';
?>

wynik

#start main# {start[1,2,3,4,5,6,7,8,9,]end} #end main#

zaawansowany przykład

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $k => $v){
    if($k === 5)
        break;
    echo $k.'=>'.$v.',';
}
echo '#end main#';
?>

wynik

#start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#

Więc wraca bez zakłócania funkcji?
Lucas Bustamante

22

yieldsłowo kluczowe służy do definicji „generatorów” w PHP 5.5. Ok, to co to jest generator ?

Z php.net:

Generatory zapewniają łatwy sposób implementacji prostych iteratorów bez narzutu i złożoności implementacji klasy, która implementuje interfejs Iterator.

Generator umożliwia pisanie kodu, który używa itach do iteracji w zestawie danych, bez potrzeby budowania tablicy w pamięci, co może spowodować przekroczenie limitu pamięci lub wygenerowanie znacznej ilości czasu. Zamiast tego możesz napisać funkcję generatora, która jest taka sama jak normalna funkcja, z tą różnicą, że zamiast zwracać jeden raz, generator może dać tyle razy, ile potrzebuje, aby podać wartości, które mają być iterowane.

Z tego miejsca: generatory = generatory, inne funkcje (tylko proste funkcje) = funkcje.

Są więc przydatne, gdy:

  • musisz robić proste rzeczy (lub proste);

    generator jest naprawdę znacznie prostszy niż implementacja interfejsu Iterator. z drugiej strony generatory są mniej funkcjonalne. porównaj je .

  • musisz wygenerować DUŻĄ ilość danych - oszczędzając pamięć;

    aby zaoszczędzić pamięć, możemy wygenerować potrzebne dane za pomocą funkcji dla każdej iteracji pętli, a po iteracji wykorzystać śmieci. więc tutaj główne punkty to - czysty kod i prawdopodobnie wydajność. zobacz, co jest lepsze dla twoich potrzeb.

  • musisz wygenerować sekwencję, która zależy od wartości pośrednich;

    jest to rozszerzenie poprzedniej myśli. generatory mogą ułatwić pracę w porównaniu z funkcjami. sprawdź przykład Fibonacciego i spróbuj utworzyć sekwencję bez generatora. Także generatory mogą pracować szybciej, przynajmniej z powodu przechowywania wartości pośrednich w zmiennych lokalnych;

  • musisz poprawić wydajność.

    w niektórych przypadkach mogą pracować szybciej niż działają (patrz poprzednia korzyść);


1
Nie rozumiałem, jak działają generatory. ta klasa implementuje interfejs iteratora. z tego, co wiem, klasy iteratorów pozwalają mi konfigurować sposób iteracji nad obiektem. na przykład ArrayIterator pobiera tablicę lub obiekt, dzięki czemu mogę modyfikować wartości i klucze podczas iteracji. więc jeśli iteratory zdobędą cały obiekt / tablicę, to w jaki sposób generator nie musi budować całej tablicy w pamięci ???
user3021621,

7

Za pomocą yieldmożna łatwo opisać punkty przerwania między wieloma zadaniami w jednej funkcji. To wszystko, nie ma w tym nic specjalnego.

$closure = function ($injected1, $injected2, ...){
    $returned = array();
    //task1 on $injected1
    $returned[] = $returned1;
//I need a breakpoint here!!!!!!!!!!!!!!!!!!!!!!!!!
    //task2 on $injected2
    $returned[] = $returned2;
    //...
    return $returned;
};
$returned = $closure($injected1, $injected2, ...);

Jeśli zadania 1 i zadanie 2 są ze sobą ściśle powiązane, ale potrzebujesz między nimi punktu przerwania, aby zrobić coś innego:

  • wolna pamięć między przetwarzaniem wierszy bazy danych
  • uruchom inne zadania, które zapewniają zależność od następnego zadania, ale które nie są ze sobą powiązane dzięki zrozumieniu bieżącego kodu
  • wykonywanie połączeń asynchronicznych i czekanie na wyniki
  • i tak dalej ...

wtedy generatory są najlepszym rozwiązaniem, ponieważ nie musisz dzielić kodu na wiele zamknięć lub mieszać go z innym kodem, ani używać wywołań zwrotnych itp. Po prostu używasz yield dodajesz punkt przerwania i możesz kontynuować od tego punkt przerwania, jeśli jesteś gotowy.

Dodaj punkt przerwania bez generatorów:

$closure1 = function ($injected1){
    //task1 on $injected1
    return $returned1;
};
$closure2 = function ($injected2){
    //task2 on $injected2
    return $returned1;
};
//...
$returned1 = $closure1($injected1);
//breakpoint between task1 and task2
$returned2 = $closure2($injected2);
//...

Dodaj punkt przerwania za pomocą generatorów

$closure = function (){
    $injected1 = yield;
    //task1 on $injected1
    $injected2 = (yield($returned1));
    //task2 on $injected2
    $injected3 = (yield($returned2));
    //...
    yield($returnedN);
};
$generator = $closure();
$returned1 = $generator->send($injected1);
//breakpoint between task1 and task2
$returned2 = $generator->send($injected2);
//...
$returnedN = $generator->send($injectedN);

Uwaga: Łatwo jest pomylić się z generatorami, więc zawsze pisz testy jednostkowe przed ich wdrożeniem! Uwaga 2: Używanie generatorów w nieskończonej pętli jest jak pisanie zamknięcia, które ma nieskończoną długość ...


4

Żadna z powyższych odpowiedzi nie pokazuje konkretnego przykładu z użyciem ogromnych tablic zapełnionych członkami nienumerycznymi. Oto przykład użycia tablicy wygenerowanej przez explode()na dużym pliku .txt (w moim przypadku 262 MB):

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

$path = './file.txt';
$content = file_get_contents($path);

foreach(explode("\n", $content) as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

Wynik był:

Starting memory usage: 415160
Final memory usage: 270948256

Teraz porównaj to do podobnego skryptu, używając yieldsłowa kluczowego:

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

function x() {
    $path = './file.txt';
    $content = file_get_contents($path);
    foreach(explode("\n", $content) as $x) {
        yield $x;
    }
}

foreach(x() as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

Dane wyjściowe dla tego skryptu były następujące:

Starting memory usage: 415152
Final memory usage: 415616

Wyraźnie oszczędności w zużyciu pamięci były znaczne (ΔMemoryUsage -----> ~ 270,5 MB w pierwszym przykładzie, ~ 450B w drugim przykładzie).


3

Ciekawym aspektem, który warto tutaj omówić, jest ustępowanie . Za każdym razem, gdy musimy zmienić parametr tak, aby był odbijany poza funkcją, musimy przekazać ten parametr przez odniesienie. Aby zastosować to do generatorów, po prostu wstawiamy znak ampersand &do nazwy generatora i zmiennej używanej w iteracji:

 <?php 
 /**
 * Yields by reference.
 * @param int $from
 */
function &counter($from) {
    while ($from > 0) {
        yield $from;
    }
}

foreach (counter(100) as &$value) {
    $value--;
    echo $value . '...';
}

// Output: 99...98...97...96...95...

Powyższy przykład pokazuje, jak zmiana iterowanych wartości w foreachpętli zmienia $fromzmienną w generatorze. Jest tak, ponieważ $fromjest uzyskiwany przez odniesienie ze względu na znak ampersand przed nazwą generatora. Z tego powodu $valuezmienna w foreachpętli jest odniesieniem do $fromzmiennej w funkcji generatora.


0

Poniższy kod ilustruje, w jaki sposób użycie generatora zwraca wynik przed zakończeniem, w przeciwieństwie do tradycyjnego podejścia bez generatora, które zwraca pełną tablicę po pełnej iteracji. Poniższy generator zwraca wartości, gdy są gotowe, nie trzeba czekać na całkowite wypełnienie tablicy:

<?php 

function sleepiterate($length) {
    for ($i=0; $i < $length; $i++) {
        sleep(2);
        yield $i;
    }
}

foreach (sleepiterate(5) as $i) {
    echo $i, PHP_EOL;
}

Więc nie jest możliwe wykorzystanie wydajności do generowania kodu HTML w php? Nie znam korzyści w prawdziwym środowisku
Giuseppe Lodi Rizzini

@GiuseppeLodiRizzini, co sprawia, że ​​tak myślisz?
Brad Kent
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.