Lo más interesante en PHP 8

PHP 7.4 acaba de ser declarado estable, y ya hemos presentado aún más mejoras. Y lo mejor de todo, lo que PHP está esperando puede decirle a Dmitry Stogov, uno de los desarrolladores líderes de PHP de código abierto y, probablemente, el colaborador activo más antiguo.

Todos los informes de Dmitry son solo sobre aquellas tecnologías y soluciones en las que él trabaja personalmente. En las mejores tradiciones de Ontiko, debajo del corte, una versión de texto de la historia sobre lo más interesante desde el punto de vista de las innovaciones de Dmitry de PHP 8, que puede abrir nuevos casos de uso. En primer lugar, JIT y FFI, no en la clave de "perspectivas sorprendentes", sino con detalles de implementación y dificultades.


Como referencia: Dmitry Stogov se familiarizó con la programación en 1984, cuando no todos los lectores nacieron, y lograron hacer una contribución significativa al desarrollo de herramientas de desarrollo, y PHP en particular (aunque Dmitry mejora el rendimiento de PHP no específicamente para los desarrolladores rusos, expresaron mi agradecimiento en forma del Premio HighLoad ++). Dmitry es el autor de Turck MMCache para PHP (eAccelerator), mantenedor de Zend OPcache, líder del proyecto PHPNG, que formó la base de PHP 7, y líder en el desarrollo de JIT para PHP.

Desarrollo de rendimiento PHP


Comencé a trabajar en el rendimiento de PHP hace 15 años cuando me uní a Zend. Luego lanzamos la versión 5.0, la primera en la que el lenguaje se volvió verdaderamente orientado a objetos. Desde entonces, hemos podido mejorar el rendimiento en pruebas sintéticas en 40 veces, y en aplicaciones reales en 6 veces.



Durante este tiempo, hubo dos momentos decisivos:

  • Versión 5.1, en la que pudimos aumentar significativamente la velocidad de interpretación. Implementamos un intérprete especializado, y esto afectó principalmente las pruebas sintéticas.
  • Versión 7.0, en la que se procesaron todas las estructuras de datos clave y, por lo tanto, optimizó el trabajo con memoria y caché del procesador (lea más sobre estas optimizaciones aquí ). Esto condujo a una aceleración de más del doble tanto en pruebas sintéticas como en aplicaciones reales.

Todas las demás versiones aumentaron gradualmente la productividad al implementar muchas ideas menos efectivas. En la versión 7.1, por ejemplo, se prestó mucha atención a la optimización de bytecode ( un artículo sobre estas soluciones).

El diagrama muestra que tanto al final del desarrollo de la quinta versión como al final del ciclo de desarrollo de la séptima versión, vamos a una meseta y disminuimos la velocidad. Entonces, durante el último año de trabajo en v7.4, solo se ha logrado un aumento del 2% en la productividad. Y esto no es malo, porque han aparecido nuevas características, como las propiedades escritas y los tipos covariantes que ralentizan PHP (Nikita Popov habló sobre estos nuevos productos en PHP Rusia).

Y ahora todos se preguntan qué esperar de la octava versión, ¿puede repetir el éxito de v7?

JIT o no JIT


Las ideas para mejorar el intérprete aún no se han agotado, pero todas requieren un estudio muy sustancial. Muchos de ellos tienen que ser rechazados en la etapa de prueba de concepto, porque la ganancia que se puede obtener resulta inconmensurable con la complicación o las limitaciones técnicas impuestas.

Pero sigue habiendo esperanza de una nueva tecnología innovadora; por supuesto, recuerdo el JIT y la historia de éxito de los motores JavaScript.

De hecho, el trabajo en JIT para PHP se lleva a cabo desde 2012. Hubo 3 o 4 implementaciones, trabajamos con colegas de Intel, hackers de JavaScript, pero de alguna manera no fue posible incluir JIT en la rama principal. Al final, en PHP 8, incluimos JIT en el compilador y vimos doble aceleración, pero solo en pruebas sintéticas, pero en aplicaciones reales, por el contrario, desaceleración.



Por supuesto, esto no es lo que buscamos.

Cual es el problema Tal vez estamos haciendo algo mal, tal vez WordPress es tan malo y ningún JIT lo ayudará (sí, en realidad lo es). Tal vez ya hemos hecho que el intérprete sea demasiado bueno, pero en JavaScript es peor. En las pruebas computacionales, esto es cierto: el intérprete PHP es uno de los mejores .



En la prueba de Mandelbrot, supera incluso gemas como LuaJIT, un intérprete escrito en lenguaje ensamblador. En esta prueba, solo estamos 4 veces detrás del compilador optimizador GCC-5.3. Con JIT, podríamos obtener mejores resultados en la prueba de Mandelbrot. En realidad, ya hacemos esto, es decir, somos capaces de generar código que compite con el compilador de C.

¿Por qué entonces no podemos acelerar las aplicaciones reales? Para entenderlo, te diré cómo hacemos JIT. Comencemos con lo básico.

Cómo funciona PHP



El servidor acepta la solicitud, la compila en bytecode, que a su vez se envía a la máquina virtual para su ejecución. Al ejecutar el bytecode, la máquina virtual también puede llamar a otros archivos PHP, que nuevamente se vuelven a compilar en bytecode y se ejecutan nuevamente.

Al completar la consulta, toda la información que se relaciona con ella, incluido el código de bytes, se elimina de la memoria. Es decir, cada script PHP debe compilarse en cada solicitud nuevamente. Por supuesto, es simplemente imposible integrar la compilación JIT en dicho esquema, porque el compilador debe ser muy rápido.

Pero lo más probable es que nadie use PHP en su forma desnuda, todos lo usan con OPcache.

PHP + OPcache



El objetivo principal de OPcache es deshacerse de la compilación de scripts en cada solicitud. Está incrustado en un punto especialmente diseñado para él, intercepta todas las solicitudes de compilación y almacena en caché el código de bytes compilado en la memoria compartida.

Al mismo tiempo, no solo se guarda el tiempo de compilación, sino también la memoria, porque la memoria de código de bytes anterior se asignó en el espacio de direcciones de cada proceso, y ahora existe en una sola copia.

Ya puedes insertar JIT en este circuito, lo que haremos. Pero primero, te mostraré cómo funciona el intérprete.



Un intérprete es ante todo un bucle que llama a su propio controlador para cada instrucción.

Usamos dos registros:

  • execute_data: puntero al marco de activación actual;
  • opline: puntero a la instrucción virtual ejecutable actual.

Usando la extensión gcc, estos dos tipos de registros se asignan a registros de hardware reales, y debido a esto funcionan muy rápidamente.

En el bucle, simplemente llamamos al controlador para cada instrucción, después de lo cual, al final de cada controlador, movemos el puntero a la siguiente instrucción.

Es importante tener en cuenta que la dirección del controlador se escribe directamente en el código de bytes. Puede haber varios manejadores diferentes para una sola instrucción. Originalmente se inventó para la especialización, de modo que los manejadores puedan especializarse en tipos de operandos. Se utiliza la misma tecnología para JIT, porque si escribe la dirección en el nuevo código generado como un controlador, los controladores JIT se iniciarán sin cambios en el intérprete.

En el ejemplo anterior, a la derecha hay un controlador escrito para la declaración de suma. Toma operandos (aquí el primero y el segundo pueden ser una variable constante, temporal o local), lee operandos, verifica tipos, produce lógica directa (suma) y luego regresa al ciclo, que transfiere el control al siguiente controlador.

Las funciones especializadas se generan a partir de esta descripción. Como había tres primeros operandos posibles, tres segundos posibles, obtenemos 9 funciones diferentes.



En estas funciones, en lugar de métodos universales para obtener operandos, se utilizan métodos específicos que no realizan ninguna comprobación.

Máquina virtual híbrida


Otra complicación que hicimos en la versión 7.2 es la llamada máquina virtual híbrida.

Si antes siempre llamamos al controlador usando una llamada indirecta directamente en el bucle de intérprete, ahora para cada controlador ingresamos adicionalmente una etiqueta en el cuerpo del bucle, al cual saltamos para usar el salto indirecto y donde llamamos al controlador en sí, pero directamente.



Parecía que antes realizaban una llamada indirecta, ahora dos: una transición indirecta y una llamada directa, y ese sistema debería funcionar más lentamente. Pero en realidad funciona más rápido, porque ayudamos al procesador a predecir las transiciones. Anteriormente, había un punto desde el cual se realizaba la transición a diferentes lugares. El procesador a menudo se equivocaba, porque simplemente no podía recordar que era necesario saltar primero en una instrucción, luego en otra. Ahora, después de cada llamada directa, hay una transición indirecta a la siguiente etiqueta. Como resultado, cuando se ejecuta el bucle PHP, las instrucciones virtuales de PHP se organizan en secuencias estables, que luego se ejecutan casi linealmente.

La máquina virtual híbrida permitió aumentar la productividad en otro 5-10%.

PHP + OPcache + JIT


JIT se implementa como parte de OPcache.



Después de compilar y optimizar el bytecode, se inicia un compilador JIT para él, que ya no funciona con el código fuente. Desde el código de bytes PHP, el compilador JIT genera código nativo, después de lo cual la dirección de la primera instrucción (de hecho, la función) se cambia en el código de bytes.

Después de eso, el código nativo, ya generado, comienza a llamarse desde el intérprete existente sin ningún cambio. Te mostraré un ejemplo simple.



A la izquierda, se escribe una determinada función en PHP que cuenta la suma de números del 0 al 100. A la derecha, el código de bytes generado. La primera instrucción asigna 0 a la suma, la segunda hace lo mismo para i, luego un salto incondicional a la etiqueta. En la etiqueta L1, se verifica la condición para salir del ciclo: si se cumple, salga, si no, vaya al ciclo. Luego, agregue a la suma i, escriba el resultado en la cantidad, aumente i en 1.

Directamente desde aquí generamos código ensamblador, que resulta bastante bueno.



La primera instrucción QM_ASSIGN compila en solo dos instrucciones de máquina (2-3 líneas). El registro %esi contiene un puntero al marco de activación actual. En el desplazamiento 30 se encuentra una cantidad variable. La primera instrucción escribe el valor 0, la segunda escribe 4: este es un identificador de un tipo entero ( IS_LONG ). Para la variable i compilador se dio cuenta de que siempre es larga y que no es necesario almacenar el tipo. Además, se puede almacenar en un registro de máquina. Por lo tanto, aquí simplemente XOR del registro consigo mismo es la instrucción más simple y económica para restablecer.

Luego, de la misma manera, una transición incondicional, verificamos si ha ocurrido algún evento externo, verificamos la condición del ciclo, entramos en el ciclo. En el ciclo, se verifica si la suma es entera: si es así, leemos el valor entero, le agregamos el valor i, verificamos si hay un desbordamiento, escribimos el resultado en la suma y agregamos 1 a %edx .

Se puede ver que el código es casi óptimo. Sería posible optimizarlo aún más, eliminando la comprobación de la suma del tipo en cada iteración del bucle. Pero esto ya es una optimización bastante complicada, todavía no lo hacemos. Estamos desarrollando JIT como una tecnología bastante simple , no estamos tratando de hacer lo que Java HotSpot está tratando de hacer, V8: tenemos menos poder.

¿Qué hay de malo con jit


¿Por qué, con un código de ensamblador tan bueno, no podemos acelerar las aplicaciones reales?

En realidad, ¿deberían?

  • Si el cuello de botella no está en la CPU, entonces JIT no ayudará.
  • Se genera demasiado código (código hinchado).
  • La inferencia de tipo estático no siempre funciona.
  • Código honesto (para casos que nunca se ejecutan).
  • Soporte para el estado consistente de la máquina virtual (y de repente una excepción).
  • Las clases viven solo para una solicitud.

Si la aplicación espera el 80% del tiempo una respuesta de la base de datos, JIT no ayudará. Si llamamos funciones externas intensivas en recursos, por ejemplo, haciendo coincidir una expresión regular, entonces JIT también llamará a las mismas funciones de la misma manera. Además, si una aplicación construye grandes estructuras de datos: árboles, gráficos y luego los lee, con la ayuda de JIT generamos código que leerá en menos instrucciones, pero para cargar los datos en sí, tomará todo el mismo tiempo, pero También deberá cargar el código.

Como ya ha visto, JIT puede incluso ralentizar una aplicación real, ya que genera mucho código y leerlo se convierte en un problema: al leer grandes cantidades de código, otros datos se expulsan del caché, lo que conduce a una desaceleración.

Planes modestos para PHP 8


Una de las mejoras que queremos lograr en PHP 8 es generar menos código . Ahora, como dije, generamos código nativo para todo el script, que cargamos en la etapa de carga. Pero la mitad de las funciones ciertamente no serán llamadas. Así que fuimos un poco más allá e introdujimos un disparador que nos permite configurar cuándo queremos ejecutar JIT. Se puede ejecutar:

  • para todas las funciones;
  • solo para funciones cuando se llaman por primera vez;
  • puede colgar un contador en cada función y compilar solo aquellas funciones que están realmente activas.

Tal esquema puede funcionar un poco mejor, pero aún no es óptimo, porque nuevamente en cada función hay rutas que se ejecutan y rutas que nunca se ejecutan. Como PHP es un lenguaje de programación dinámico, es decir, cada variable puede tener diferentes tipos, resulta que necesita admitir todos los tipos que predice el analizador estático. Y a menudo lo hace con precaución cuando no pudo probar que el otro tipo no podía hacerlo.

En estas condiciones, nos alejaremos de la compilación honesta y comenzaremos a hacerlo especulativamente.



En el futuro, planeamos primero durante algún tiempo durante el trabajo de la aplicación analizar las funciones "más populares", ver qué rutas recorre el programa, qué tipos de variables son, tal vez incluso recordar las condiciones de contorno, y solo entonces generar el código de función óptimo para la corriente forma de ejecución: solo para aquellas secciones que realmente se ejecutan.

Para todo lo demás, pondremos talones. De todos modos, habrá comprobaciones y posibles salidas en las que comenzará el proceso de desoptimización, es decir, restauraremos el estado de la máquina virtual necesaria para la interpretación y se la daremos al intérprete para su ejecución.

Se utiliza un esquema similar en HotSpot Java VM y V8. Pero adaptar la tecnología a PHP tiene varias dificultades. En primer lugar, esto es que hemos compartido bytecode y código nativo compartido utilizado desde diferentes procesos. No podemos cambiarlos directamente en la memoria compartida, primero debemos copiar en algún lugar, cambiar y luego volver a confirmar en la memoria compartida.

Precarga El problema de la vinculación de clases


De hecho, muchas de las ideas para mejoras de PHP que se han incluido durante mucho tiempo en PHP 7 e incluso PHP 5 provienen del trabajo relacionado con JIT. Hoy hablaré sobre otra tecnología de este tipo: esta es la precarga. Esta tecnología ya está incluida en PHP 7.4 y permite especificar un conjunto de archivos, cargarlos al inicio del servidor y hacer que todas las funciones de estos archivos sean permanentes.

Uno de los problemas que resuelve la tecnología de precarga es el problema del enlace de clases. El hecho es que cuando simplemente compilamos archivos en PHP, cada archivo se compila por separado de los demás. Esto se hace porque cada uno de ellos se puede cambiar por separado. No puede asociar una clase de un script con una clase de otro script, porque en la próxima solicitud uno de ellos puede cambiar y algo saldrá mal. Además, en varios archivos puede haber una clase con el mismo nombre, y con una solicitud se usa uno de ellos como padre, y con el otro, se usa otra clase de otro archivo (con el mismo nombre, pero con uno completamente diferente). Resulta que al generar código que se ejecutará en varias solicitudes, no puede hacer referencia a clases o métodos, porque se recrean cada vez (la vida útil del código excede la vida útil de la clase).

La precarga le permite vincular las clases inicialmente y, en consecuencia, generar el código de manera más óptima. Como mínimo, para los marcos que se cargarán mediante la precarga.

Esta tecnología ayuda no solo para el enlace de clases. Algo similar se implementa en Java como Class Data Sharing. Allí, esta tecnología está destinada principalmente a acelerar el lanzamiento de aplicaciones y reducir la cantidad total de memoria consumida. Las mismas ventajas se obtienen en PHP, porque ahora el enlace de clase no se realiza en tiempo de ejecución, sino que se realiza solo una vez. Además, las clases asociadas ahora se almacenan no en el espacio de direcciones de cada proceso, sino en la memoria compartida y, por lo tanto, el consumo total de memoria disminuye.

El uso de la precarga también ayuda con la optimización global de todos los scripts PHP, elimina por completo la sobrecarga de OPcache y le permite generar un código JIT más eficiente.

Pero también hay desventajas. Las secuencias de comandos cargadas al inicio no se pueden reemplazar sin reiniciar PHP. Si descargamos algo y lo hicimos permanente, ya no podemos descargarlo. Por lo tanto, la tecnología se puede usar con marcos estables, pero si implementa la aplicación varias veces al día, lo más probable es que no funcione para usted.

La tecnología se concibió como transparente, es decir, permitió cargar aplicaciones existentes (o partes de las mismas) sin ningún cambio. Pero después de la implementación, resultó que esto no es del todo cierto. No todas las aplicaciones funcionan como se pretendía si se cargaran con precarga . Por ejemplo, si se llama a un código en la aplicación en función de los resultados de la comprobación de function_exists o class_exists , y la función se vuelve constante, respectivamente, function_exists siempre devuelve true , y se suponía que se debía llamar al código que se llamó previamente.

Técnicamente, la precarga se habilita con solo una directiva de configuración opcache.preload, a la entrada de la cual se le da un archivo de script, un archivo PHP normal que se iniciará en la etapa de inicio de la aplicación (no solo cargado, sino ejecutado).

 <?php function _preload(string $preload, string $pattern = "/\.php$/") { if (is_file($path) && preg_match($pattern, $path)) { opcache_compile_file($path) or die("Preloading failed"); } else if (is_dir($path)) { if ($dh = opendir($path)) { while (($file = readdir($dh)) !== false) { if ($file !== "." && $file !== "..") { _preload($path . "/" . $file, $pattern); } } closedir($dh); } } } _preload("/usr/local/lib/ZendFramework"); 

Este es uno de los escenarios posibles que lee recursivamente todos los archivos en algún directorio (en este caso, ZendFramework). Puede implementar absolutamente cualquier secuencia de comandos en PHP: lea con una lista, agregue excepciones o incluso cruce con el compositor para que pueda colocar los archivos necesarios para la precarga. Todo esto es cuestión de tecnología, y lo más interesante no es cómo enviar, sino qué enviar.

Qué cargar en precarga


Probé esta tecnología en WordPress. Si solo carga todos los archivos * .php, WordPress dejará de funcionar debido a la función mencionada anteriormente: tiene una comprobación de función_existe, que siempre se cumple. Por lo tanto, tuve que modificar ligeramente el script del ejemplo anterior (agregar excepciones), y luego, sin ningún cambio en WordPress, funcionó.

Velocidad [req / seq]Memoria [MB]Numero de guionesNumero de funcionesNumero de clases
Nada3780 00 00 00 0
Todos (casi *)3957.52541770148
Solo guiones usados3964,584153251

Como resultado, debido a la precarga, obtuvimos una aceleración de ~ 5% , lo que ya no está mal.

Descargué casi todos los archivos, pero la mitad de ellos no se usaron. Puede hacerlo aún mejor: maneje la aplicación, vea qué archivos se han descargado. Puede hacerlo utilizando la función opcache_get_status() , que devolverá todos los archivos en caché de OPcache y creará una lista para su precarga. Por lo tanto, puede ahorrar 3 MB y obtener un poco más de aceleración. El hecho es que cuanto más memoria se necesita, más caché del procesador se ensucia y menos eficiente es. Mientras menos memoria se use, mayor será la velocidad.

FFI - Interfaz de función externa


Otra tecnología relacionada con JIT que se desarrolló para PHP es FFI (Foreign Function Interface) o, en ruso, la capacidad de llamar funciones escritas en otros lenguajes de programación compilados sin compilación. Las implementaciones de dicha tecnología en Python impresionaron a mi jefe (Zeev Surazki), y quedé muy impresionado cuando comencé a adaptarlo a PHP.

Ya ha habido varios intentos en PHP para crear una extensión para FFI, pero todos usaron su propio lenguaje o API para describir las interfaces. Vi la idea en LuaJIT, donde se usa el lenguaje C (un subconjunto) para describir las interfaces, y el resultado es un juguete genial. Ahora, cuando necesito verificar cómo funciona algo en C, lo escribo en PHP; sucede, directamente en la línea de comandos.

FFI le permite trabajar con estructuras de datos definidas en C y puede integrarse con JIT para generar código más eficiente. Su implementación basada en libffi ya está incluida en PHP 7.4.

Pero:

  • Estas son 1000 nuevas formas de dispararte en el pie.
  • Requiere conocimiento de C y, a veces, gestión manual de la memoria.
  • No es compatible con el preprocesador C (#include, #define, ...) y C ++.
  • El rendimiento sin JIT es bastante bajo.

Aunque, tal vez para algunos sea conveniente, porque el compilador no es necesario. Incluso en Windows, esto funcionará sin ningún Visual-C de PHP.

Le mostraré cómo usar FFI para implementar una aplicación GUI real para Linux.

No se alarme por el código C, yo mismo escribí una GUI en C hace unos 20 años, pero encontré este ejemplo en Internet.

 #include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer user_data) { GtkWidget *window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Hello from C"); gtk_window_set_default_size(GTK_WINDOW(window), 200, 200); gtk_widget_show_all(window); } int main() { int status; GtkApplication *app; app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), 0, NULL); g_object_unref(app); return status; } 

El programa crea la aplicación, se bloquea en el evento de devolución de llamada de activación, inicia la aplicación. En la devolución de llamada, cree una ventana, asígnele el tamaño del título y muéstrela.

Y ahora, lo mismo reescrito en PHP:

 <?php $ffi = FFI::cdef(" … // #include <gtk/gtk.h> ", "libgtk-3.so.0"); function activate($app, $user_data) { global $ffi; $window = $ffi->gtk_application_window_new($app); $ffi->gtk_window_set_title($window, "Hello from PHP"); $ffi->gtk_window_set_default_size($window, 200, 200); $ffi->gtk_widget_show_all($window); } $app = $ffi->gtk_application_new("org.gtk.example", 0); $ffi->g_signal_connect_data($app, "activate", "activate", NULL, NULL, 0); $ffi->g_application_run($app, 0, NULL); $ffi->g_object_unref($app); 

Aquí, el objeto FFI se crea primero. Se le envía una descripción de la interfaz como entrada, en esencia, un archivo h, y la biblioteca que queremos descargar. Después de eso, todas las funciones descritas en la interfaz están disponibles como métodos del objeto ffi, y todos los parámetros transferidos se traducen de forma automática y absolutamente transparente en la representación de máquina necesaria.

Se puede ver que todo es exactamente igual que en el ejemplo anterior. La única diferencia es que en C enviamos una devolución de llamada como dirección, y en PHP, la conexión se produce por el nombre dado por la cadena.

Ahora veamos cómo se ve la interfaz. En la primera parte, determinamos los tipos y funciones en C, y en la última línea cargamos la biblioteca compartida:

 <?php $ffi = FFI::cdef(" typedef struct _GtkApplication GtkApplication; typedef struct _GtkWidget GtkWidget; typedef void (*GCallback)(void*,void*); int g_application_run (GtkApplication *app, int argc, char **argv); unsigned long * g_signal_connect_data (void *ptr, const char *signal, GCallback handler, void *data, GCallback *destroy, int flags); void g_object_unref (void *ptr); GtkApplication * gtk_application_new (const char *app_id, int flags); GtkWidget * gtk_application_window_new (GtkApplication *app); void gtk_window_set_title (GtkWidget *win, const char *title); void gtk_window_set_default_size (GtkWidget *win, int width, int height); void gtk_widget_show_all (GtkWidget *win); ", "libgtk-3.so.0"); ... 

En este caso, estas definiciones C se copian de los archivos h de la biblioteca GTK, casi sin cambios.

Para no interferir con C y PHP en el mismo archivo, puede poner todo el código C en un archivo separado, por ejemplo, con el nombre gtk-ffi.h y agregar un par de define'ov especiales al principio que especifiquen el nombre de la interfaz y la biblioteca para cargar:

 #define FFI_SCOPE "GTK" #define FFI_LIB "libgtk-3.so.0" 

Por lo tanto, seleccionamos la descripción completa de la interfaz C en un archivo. Este gtk-ffi.h es casi real, pero desafortunadamente, aún no hemos implementado un preprocesador C, lo que significa que las macros e inclusiones no funcionarán.

Ahora carguemos esta interfaz en PHP:

 <?php final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::load(__DIR__ . "/gtk_ffi.h"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

Dado que FFI es una tecnología bastante peligrosa, no queremos dársela a nadie. Al menos, ocultemos el objeto FFI, es decir, hagámoslo privado dentro de la clase. Y crearemos un objeto FFI que no use FFI::cdef , sino que use FFI::load , que lee solo nuestro archivo h del ejemplo anterior.

El resto del código no ha cambiado mucho, solo como un controlador de eventos comenzamos a usar una función sin nombre y pasamos el título usando el enlace léxico. Es decir, utilizamos C y las fortalezas de PHP, que no están disponibles en C.

Una biblioteca creada de esta manera ya podría usarse en su aplicación. Pero es bueno si funciona solo en la línea de comandos , y si lo coloca dentro del servidor web, entonces, en cada solicitud, se leerá el archivo gtk_ffi.h, se creará y cargará una biblioteca, se realizará un enlace ... Y todo este trabajo repetitivo cargará su servidor.

Para evitar esto y, de hecho, permitir escribir extensiones PHP en el propio PHP, decidimos cruzar FFI con precarga.

Precarga FFI +


El código no ha cambiado mucho, solo que ahora asignamos los archivos h a la precarga, y ejecutamos FFI::load directamente en el momento de la precarga, y no cuando creamos el objeto. Es decir, al cargar la biblioteca, todos los análisis y enlaces se realizan una vez (cuando se inicia el servidor), y usando FFI::scope("GTK") obtenemos acceso a la interfaz precargada por nombre en nuestro script.

 <?php FFI::load(__DIR__ . "/gtk_ffi.h"); final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::scope("GTK"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

En esta realización, FFI puede usarse desde un servidor web. Por supuesto, esto no es para la GUI, pero de esta manera puede escribir, por ejemplo, un enlace a la base de datos.

Una extensión creada de esta manera se puede usar directamente desde la línea de comando:
 $ php -d opcache.preload=gtk.php -r 'GTK::create_window(" !");' 

Otra ventaja del cruce y precarga de FFI es la capacidad de prohibir el uso de FFI para todos los scripts de nivel de usuario. Puede especificar ffi.enable = preload, lo que significa que confiamos en los archivos precargados, pero está prohibido llamar a FFI desde scripts PHP normales.

Trabajando con estructuras de datos C


Otra característica interesante de FFI es que puede funcionar con estructuras de datos nativas. En cualquier momento puede crear en la memoria cualquier estructura de datos descrita en C.

 <?php $points = FFI::new("struct {int x,y;} [100]"); for ($x = 0; $x < count($points); $x++) { $points[$x]->x = $x; $points[$x]->y = $x * $x; } var_dump($points[25]->y); // 625 var_dump(FFI::sizeof($points)); // 800  foreach ($points as &$p) { $p->x += 10; } var_dump($points[25]->x); // 35 

100 ( FFI::new != new FFI), integer. , C. PHP, . count, / foreach . 800 , PHP PHP' , 10 .

FFI:

  • PHP.
  • PHP.

Python/CFFI : (Cario, JpegTran), (ffmpeg), (LibreOfficeKit), (SDL) (TensorFlow).

, FFI .

- PHP. , , callback' , . FFI. , . FFI c JIT, , LuaJIT, . , , .

 for ($k=0; $k<1000; $k++) { for ($i=$n-1; $i>=0; $i--) { $Y[$i] += $X[$i]; } } 

FFI .
Native ArraysFFI Arrays
PyPy0,0100,081
Python0,2120,343
LuaJIt -joff0,0370,412
LuaJit -jon0,0030,002
Php0,0400,093
PHP + JIT0,0160,087

: Zeev Surasky (Zend), Andi Gutmans (ex-Zend, Amazon), Xinchen Hui (ex-Weibo, ex-Zend, Lianjia), Nikita Popov (JetBrains), Anatol Belsky (Microsoft), Anthony Ferrara (ex-Google, Lingo Live), Joe Watkins, Mohammad Reza Haghighat (Intel) Intel, Andy Wingo (JS hacker, Igalia), Mike Pall ( LuaJIT).

, , .

PHP Russia 2020 ! telegram- , 2019 youtube- , , — .

Source: https://habr.com/ru/post/481092/


All Articles