¿Qué significa rendimiento en PHP?

232

Recientemente me topé con este código:

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

Nunca he visto esta yieldpalabra clave antes. Intentando ejecutar el código que obtengo

Error de análisis: error de sintaxis, T_VARIABLE inesperado en la línea x

Entonces, ¿cuál es esta yieldpalabra clave? ¿Es incluso válido PHP? Y si es así, ¿cómo lo uso?

Gordon
fuente

Respuestas:

355

¿Qué es yield?

La yieldpalabra clave devuelve datos de una función generadora:

El corazón de una función generadora es la palabra clave de rendimiento. En su forma más simple, una declaración de rendimiento se parece mucho a una declaración de retorno, excepto que en lugar de detener la ejecución de la función y regresar, el rendimiento proporciona un valor al código que se repite sobre el generador y detiene la ejecución de la función del generador.

¿Qué es una función generadora?

Una función de generador es efectivamente una forma más compacta y eficiente de escribir un iterador . Le permite definir una función (su xrange) que calculará y devolverá valores mientras realiza un bucle sobre ella :

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

Esto crearía el siguiente resultado:

0 => 1
1 => 2

9 => 10

También puede controlar el $keyen el foreachmediante

yield $someKey => $someValue;

En la función de generador, $someKeyes lo que desea que aparezca $keyy $someValuees el valor en $val. En el ejemplo de la pregunta, eso es $i.

¿Cuál es la diferencia con las funciones normales?

Ahora puede preguntarse por qué no estamos simplemente usando la rangefunción nativa de PHP para lograr esa salida. Y tienes razón. La salida sería la misma. La diferencia es cómo llegamos allí.

Cuando usamos rangePHP, lo ejecutaremos, crearemos toda la matriz de números en la memoria y todareturn esa matriz en el foreachciclo que luego lo recorrerá y generará los valores. En otras palabras, foreachoperará en la matriz misma. La rangefunción y la foreachúnica "charla" una vez. Piense en ello como recibir un paquete por correo. El repartidor te entregará el paquete y se irá. Y luego desenvuelves todo el paquete, sacando lo que esté allí.

Cuando usamos la función de generador, PHP entrará en la función y la ejecutará hasta que cumpla con el final o una yieldpalabra clave. Cuando se encuentra con a yield, devolverá lo que sea el valor en ese momento al bucle externo. Luego vuelve a la función de generador y continúa desde donde cedió. Como tu xrangemantiene un forbucle, se ejecutará y cederá hasta que $maxse alcance. Piense en ello como el foreachy el generador jugando ping pong.

¿Por qué necesito eso?

Obviamente, los generadores se pueden usar para evitar los límites de memoria. Dependiendo de su entorno, hacer un range(1, 1000000)testamento fatal en su script, mientras que lo mismo con un generador funcionará bien. O como dice Wikipedia:

Debido a que los generadores calculan sus valores producidos solo bajo demanda, son útiles para representar secuencias que serían costosas o imposibles de calcular de una vez. Estos incluyen, por ejemplo, secuencias infinitas y secuencias de datos en vivo.

También se supone que los generadores son bastante rápidos. Pero tenga en cuenta que cuando hablamos de rápido, generalmente hablamos en números muy pequeños. Entonces, antes de salir corriendo y cambiar todo el código para usar generadores, haga un punto de referencia para ver dónde tiene sentido.

Otro caso de uso para generadores son las rutinas asíncronas. La yieldpalabra clave no solo devuelve valores, sino que también los acepta. Para obtener detalles sobre esto, consulte las dos excelentes publicaciones de blog vinculadas a continuación.

¿Desde cuándo puedo usar yield?

Se han introducido generadores en PHP 5.5 . Intentar usar yieldantes de esa versión generará varios errores de análisis, dependiendo del código que sigue a la palabra clave. Entonces, si obtiene un error de análisis de ese código, actualice su PHP.

Fuentes y lecturas adicionales:

Gordon
fuente
1
Explique cuáles son los beneficios de yeild, digamos, una solución como esta: ideone.com/xgqevM
Mike
1
Ah, bueno, y los avisos que estaba generando. Huh Bueno, experimenté emulando Generadores para PHP> = 5.0.0 con una clase auxiliar, y sí, un poco menos legible, pero puedo usar esto en el futuro. Tema interesante. ¡Gracias!
Mike
¡No legibilidad sino uso de memoria! Compare la memoria usada para iterar return range(1,100000000)y for ($i=0; $i<100000000; $i++) yield $i
emix
@ Mike, sí, eso ya se explicó en mi respuesta. En el otro ejemplo, la memoria de Mike no es un problema porque solo está iterando 10 valores.
Gordon
1
@Mike Un problema con la xrange es que su uso de límites estáticos es la utilidad para anidar, por ejemplo (por ejemplo, buscar en una variedad n dimensional, o una clasificación rápida recursiva usando generadores, por ejemplo). No puede anidar bucles xrange porque solo hay una instancia de su contador. La versión Yield no sufre este problema.
Shayne
43

Esta función está usando rendimiento:

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

es casi lo mismo que este sin:

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

La única diferencia es que a()devuelve un generador y b()solo una matriz simple. Puedes iterar en ambos.

Además, el primero no asigna una matriz completa y, por lo tanto, requiere menos memoria.

tsusanka
fuente
2
notas adicionales de los documentos oficiales: en PHP 5, un generador no podía devolver un valor: al hacerlo se generaría un error de compilación. Una declaración de retorno vacía era una sintaxis válida dentro de un generador y terminaría el generador. Desde PHP 7.0, un generador puede devolver valores, que pueden recuperarse usando Generator :: getReturn (). php.net/manual/en/language.generators.syntax.php
Programador Dancuk
Simple y conciso.
John Miller
24

ejemplo simple

<?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#';
?>

salida

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

ejemplo avanzado

<?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#';
?>

salida

#start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#
Piensa en grande
fuente
Entonces, ¿vuelve sin interrumpir la función?
Lucas Bustamante
22

yieldLa palabra clave sirve para la definición de "generadores" en PHP 5.5. Ok, entonces, ¿qué es un generador ?

Desde php.net:

Los generadores proporcionan una manera fácil de implementar iteradores simples sin la sobrecarga o la complejidad de implementar una clase que implemente la interfaz Iterator.

Un generador le permite escribir código que usa foreach para iterar sobre un conjunto de datos sin necesidad de construir una matriz en la memoria, lo que puede hacer que exceda un límite de memoria o que requiera una cantidad considerable de tiempo de procesamiento para generar. En cambio, puede escribir una función de generador, que es lo mismo que una función normal, excepto que en lugar de regresar una vez, un generador puede ceder tantas veces como sea necesario para proporcionar los valores que se repetirán.

Desde este lugar: generadores = generadores, otras funciones (solo funciones simples) = funciones.

Por lo tanto, son útiles cuando:

  • necesitas hacer cosas simples (o cosas simples);

    El generador es realmente mucho más simple que implementar la interfaz Iterator. Por otro lado, es de origen que los generadores son menos funcionales. compáralos .

  • necesita generar GRANDES cantidades de datos, ahorrando memoria;

    En realidad, para ahorrar memoria, podemos generar los datos necesarios a través de funciones para cada iteración de bucle, y después de la iteración utilizar basura. así que aquí los puntos principales son: código claro y probablemente rendimiento. Vea qué es mejor para sus necesidades.

  • necesita generar una secuencia, que depende de valores intermedios;

    Esto se extiende al pensamiento anterior. Los generadores pueden facilitar las cosas en comparación con las funciones. verifique el ejemplo de Fibonacci e intente hacer una secuencia sin generador. También los generadores pueden trabajar más rápido en este caso, al menos debido al almacenamiento de valores intermedios en variables locales;

  • Necesitas mejorar el rendimiento.

    pueden trabajar más rápido que las funciones en algunos casos (ver beneficio anterior);

QArea
fuente
1
No entendí cómo funcionan los generadores. Esta clase implementa la interfaz de iterador. Por lo que sé, las clases de iteradores me permiten configurar cómo quiero iterar sobre un objeto. por ejemplo, ArrayIterator obtiene una matriz u objeto para que pueda modificar valores y claves mientras lo itera. Entonces, si los iteradores obtienen todo el objeto / matriz, ¿cómo no tiene que generar el generador toda la matriz en la memoria?
user3021621
7

Con yieldusted puede describir fácilmente los puntos de interrupción entre múltiples tareas en una sola función. Eso es todo, no tiene nada de especial.

$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, ...);

Si task1 y task2 están altamente relacionadas, pero necesita un punto de interrupción entre ellas para hacer otra cosa:

  • memoria libre entre las filas de procesamiento de la base de datos
  • ejecutar otras tareas que proporcionan dependencia a la siguiente tarea, pero que no están relacionadas al comprender el código actual
  • haciendo llamadas asincrónicas y esperando los resultados
  • y así ...

entonces los generadores son la mejor solución, porque no tiene que dividir su código en muchos cierres o mezclarlo con otro código, o usar devoluciones de llamada, etc. Simplemente use yieldpara agregar un punto de interrupción, y puede continuar desde allí. punto de interrupción si está listo.

Agregar punto de interrupción sin generadores:

$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);
//...

Agregar punto de interrupción con generadores

$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);

nota: es fácil cometer errores con los generadores, ¡así que siempre escriba pruebas unitarias antes de implementarlas! nota2: Usar generadores en un bucle infinito es como escribir un cierre que tiene una longitud infinita ...

inf3rno
fuente
4

Ninguna de las respuestas anteriores muestra un ejemplo concreto usando matrices masivas pobladas por miembros no numéricos. Aquí hay un ejemplo usando una matriz generada por explode()un archivo .txt grande (262MB en mi caso de uso):

<?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();

El resultado fue:

Starting memory usage: 415160
Final memory usage: 270948256

Ahora compare eso con un script similar, usando la yieldpalabra clave:

<?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();

El resultado de este script fue:

Starting memory usage: 415152
Final memory usage: 415616

Claramente, el ahorro en el uso de memoria fue considerable (ΔMemoryUsage -----> ~ 270.5 MB en el primer ejemplo, ~ 450B en el segundo ejemplo).

David Partyka
fuente
3

Un aspecto interesante, que vale la pena discutir aquí, está cediendo por referencia . Cada vez que necesitamos cambiar un parámetro de modo que se refleje fuera de la función, tenemos que pasar este parámetro por referencia. Para aplicar esto a los generadores, simplemente anteponemos un ampersand &al nombre del generador y a la variable utilizada en la iteración:

 <?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...

El ejemplo anterior muestra cómo cambiar los valores iterados dentro del foreachbucle cambia la $fromvariable dentro del generador. Esto se debe a que $fromse obtiene por referencia debido al signo y antes del nombre del generador. Debido a eso, la $valuevariable dentro del foreachciclo es una referencia a la $fromvariable dentro de la función del generador.

Bud Damyanov
fuente
0

El siguiente código ilustra cómo el uso de un generador devuelve un resultado antes de la finalización, a diferencia del enfoque tradicional sin generador que devuelve una matriz completa después de la iteración completa. Con el siguiente generador, los valores se devuelven cuando están listos, no es necesario esperar a que se complete por completo una matriz:

<?php 

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

foreach (sleepiterate(5) as $i) {
    echo $i, PHP_EOL;
}
Risteard
fuente
Entonces, ¿no es posible usar el rendimiento para generar código html en php? No conozco los beneficios en un entorno real
Giuseppe Lodi Rizzini
@GiuseppeLodiRizzini, ¿qué te hace pensar eso?
Brad Kent,