A coisa mais interessante no PHP 8

O PHP 7.4 acabou de ser declarado estável, e já submetemos ainda mais melhorias. E o melhor de tudo, o que o PHP está esperando pode dizer a Dmitry Stogov - um dos principais desenvolvedores do PHP de código aberto e, provavelmente, o colaborador ativo mais antigo.

Todos os relatórios de Dmitry são apenas sobre as tecnologias e soluções nas quais ele trabalha pessoalmente. Nas melhores tradições da Ontiko, abaixo do corte, uma versão em texto da história sobre o mais interessante, do ponto de vista das inovações de Dmitry no PHP 8, que pode abrir novos casos de uso. Primeiro de tudo, JIT e FFI - não na chave das “perspectivas incríveis”, mas com detalhes e armadilhas da implementação.


Para referência: Dmitry Stogov se familiarizou com a programação em 1984, quando nem todos os leitores nasceram, e conseguiu dar uma contribuição significativa ao desenvolvimento de ferramentas de desenvolvimento e ao PHP em particular (embora Dmitry melhore o desempenho do PHP não especificamente para desenvolvedores russos, eles expressaram meus agradecimentos na forma do prêmio HighLoad ++). Dmitry é o autor do Turck MMCache para PHP (eAccelerator), mantenedor do Zend OPcache, líder do projeto PHPNG, que formou a base do PHP 7, e líder no desenvolvimento do JIT para PHP.

Desenvolvimento de desempenho PHP


Comecei a trabalhar no desempenho do PHP há 15 anos quando entrei no Zend. Em seguida, lançamos a versão 5.0 - a primeira na qual a linguagem se tornou verdadeiramente orientada a objetos. Desde então, conseguimos melhorar o desempenho em testes sintéticos em 40 vezes e em aplicativos reais em 6 vezes.



Durante esse período, houve dois momentos inovadores:

  • Versão 5.1, na qual conseguimos aumentar significativamente a velocidade da interpretação. Implementamos um intérprete especializado, o que afetou principalmente os testes sintéticos.
  • Versão 7.0, na qual todas as estruturas de dados principais foram processadas e, portanto, otimizaram o trabalho com memória e cache do processador (leia mais sobre essas otimizações aqui ). Isso levou a aceleração mais que dupla, tanto em testes sintéticos quanto em aplicações reais.

Todas as outras versões aumentaram gradualmente a produtividade implementando muitas idéias menos eficazes. Na versão 7.1, por exemplo, muita atenção foi dada à otimização do bytecode ( um artigo sobre essas soluções).

O diagrama mostra que, no final do desenvolvimento da 5ª versão e no final do ciclo de desenvolvimento da 7ª versão, vamos para um platô e desaceleramos. Portanto, durante o último ano de trabalho na v7.4, apenas um aumento de 2% na produtividade foi alcançado. E isso não é ruim, porque novos recursos, como propriedades digitadas e tipos covariantes, apareceram que desaceleram o PHP (Nikita Popov falou sobre esses novos produtos no PHP Rússia).

E agora todo mundo está se perguntando o que esperar da 8ª versão, ele pode repetir o sucesso da v7?

Para JIT ou não para JIT


As idéias para melhorar o intérprete ainda não foram esgotadas, mas todas elas requerem um estudo muito substancial. Muitos deles precisam ser rejeitados na fase de prova de conceito, porque o ganho que pode ser obtido é incomensurável com as complicações ou restrições técnicas impostas.

Mas ainda há esperança de uma nova tecnologia inovadora - é claro, eu me lembro do JIT e da história de sucesso dos mecanismos JavaScript.

De fato, o trabalho no JIT para PHP está em andamento desde 2012. Havia 3 ou 4 implementações, trabalhamos com colegas da Intel, hackers JavaScript, mas de alguma forma não foi possível incluir o JIT no ramo principal. No final, no PHP 8, incluímos o JIT no compilador e vimos aceleração dupla, mas apenas em testes sintéticos, mas em aplicativos reais, pelo contrário, desaceleração.



Obviamente, não é por isso que estamos nos esforçando.

Qual é o problema? Talvez estejamos fazendo algo errado, talvez o WordPress seja tão ruim e nenhum JIT o ajude (sim, na verdade é). Talvez já tenhamos tornado o intérprete muito bom, mas em JavaScript é pior. Nos testes computacionais, isso é verdade: o intérprete PHP é um dos melhores .



No teste de Mandelbrot, ele ultrapassa até gemas como LuaJIT, um intérprete escrito em linguagem assembly. Neste teste, estamos apenas quatro vezes atrás do compilador GCC-5.3 otimizador. Com o JIT, conseguimos melhores resultados no teste de Mandelbrot. Na verdade, já fazemos isso, ou seja, somos capazes de gerar código que compete com o compilador C.

Por que, então, não podemos acelerar aplicativos reais? Para entender, vou lhe dizer como fazemos o JIT. Vamos começar com o básico.

Como o PHP funciona



O servidor aceita a solicitação, compila-a no bytecode, que por sua vez é enviado à máquina virtual para execução. Ao executar o bytecode, a máquina virtual também pode chamar outros arquivos PHP, que são recompilados novamente no bytecode e executados novamente.

Após a conclusão da consulta, todas as informações relacionadas a ela, incluindo o código de bytes, são excluídas da memória. Ou seja, cada script PHP deve ser compilado em cada solicitação novamente. Obviamente, é simplesmente impossível incorporar a compilação JIT em um esquema desse tipo, porque o compilador deve ser muito rápido.

Mas provavelmente ninguém usa PHP em sua forma simples, todo mundo usa com OPcache.

PHP + OPcache



O principal objetivo do OPcache é livrar-se da recompilação de scripts em cada solicitação. Ele é incorporado em um ponto especialmente projetado para ele, intercepta todas as solicitações de compilação e armazena em cache o bytecode compilado na memória compartilhada.

Ao mesmo tempo, não apenas o tempo de compilação é salvo, mas também a memória, porque a memória do bytecode anterior foi alocada no espaço de endereço de cada processo e agora existe em uma única cópia.

Você já pode incorporar o JIT neste circuito, o que faremos. Mas primeiro, mostrarei como o intérprete funciona.



Um intérprete é antes de tudo um loop que chama seu próprio manipulador para cada instrução.

Usamos dois registros:

  • execute_data - ponteiro para o quadro de ativação atual;
  • opline - ponteiro para a instrução virtual executável atual.

Usando a extensão gcc, esses dois tipos de registradores são mapeados para registros reais de hardware e, devido a isso, eles funcionam muito rapidamente.

No loop, simplesmente chamamos o manipulador para cada instrução, após o que, no final de cada manipulador, movemos o ponteiro para a próxima instrução.

É importante observar que o endereço do manipulador é gravado diretamente no bytecode. Pode haver vários manipuladores diferentes para uma única instrução. Isso foi originalmente inventado para especialização, para que os manipuladores possam se especializar nos tipos de operandos. A mesma tecnologia é usada para o JIT, porque se você escrever o endereço no novo código gerado como manipulador, os manipuladores JIT serão iniciados sem nenhuma alteração no intérprete.

No exemplo acima, o manipulador escrito para a instrução de adição é apresentado à direita. Ele pega operandos (aqui o primeiro e o segundo podem ser uma variável constante, temporária ou local), lê operandos, verifica tipos, produz lógica direta - adição - e depois retorna ao loop, que transfere o controle para o próximo manipulador.

Funções especializadas são geradas a partir desta descrição. Como havia três primeiros operandos possíveis, três segundos possíveis, obtemos 9 funções diferentes.



Nessas funções, em vez de métodos universais para obter operandos, são utilizados métodos específicos que não fazem nenhuma verificação.

Máquina Virtual Híbrida


Outra complicação que fizemos na versão 7.2 é a chamada máquina virtual híbrida.

Se anteriormente sempre chamamos o manipulador usando uma chamada indireta diretamente no loop do interpretador, agora para cada manipulador inserimos adicionalmente um rótulo no corpo do loop, para o qual saltamos para o salto indireto e onde chamamos o próprio manipulador, mas diretamente.



Parecia que antes eles faziam uma ligação indireta, agora duas: uma transição indireta e uma ligação direta, e esse sistema deveria funcionar mais devagar. Mas, na verdade, ele funciona mais rápido, porque ajudamos o processador a prever transições. Anteriormente, havia um ponto a partir do qual a transição para lugares diferentes era realizada. O processador costumava estar enganado, porque simplesmente não conseguia se lembrar de que era necessário pular primeiro uma instrução e depois outra. Agora, após cada chamada direta, há uma transição indireta para o próximo rótulo. Como resultado, quando o loop do PHP é executado, as instruções virtuais do PHP são organizadas em seqüências estáveis, que são executadas quase linearmente.

A máquina virtual híbrida permitiu aumentar a produtividade em mais 5 a 10%.

PHP + OPcache + JIT


O JIT é implementado como parte do OPcache.



Depois que o bytecode é compilado e otimizado, um compilador JIT é lançado para ele, que não funciona mais com o código-fonte. No bytecode do PHP, o compilador JIT gera código nativo, após o qual o endereço da primeira instrução (de fato a função) é alterado no bytecode.

Depois disso, o código nativo já gerado começa a ser chamado a partir do intérprete existente sem nenhuma alteração. Vou mostrar um exemplo simples.



À esquerda, uma certa função é escrita em PHP que conta a soma dos números de 0 a 100. À direita, o código de código gerado. A primeira instrução atribui 0 à soma, a segunda faz o mesmo para i, depois um salto incondicional para o rótulo. Na etiqueta L1, a condição para sair do ciclo é verificada: se estiver preenchida, saia, se não, então vá para o ciclo. Em seguida, adicione à soma i, escreva o resultado na quantidade, aumente i em 1.

Diretamente daqui, geramos código assembler, o que resulta muito bom.



A primeira instrução QM_ASSIGN compilada em apenas duas instruções da máquina (2-3 linhas). O registro %esi contém um ponteiro para o quadro de ativação atual. No deslocamento 30, encontra-se uma quantidade variável. A primeira instrução grava o valor 0, a segunda grava 4 - este é um identificador de um tipo inteiro ( IS_LONG ). Para a variável i compilador percebeu que é sempre longo e, para isso, não há necessidade de armazenar o tipo. Além disso, ele pode ser armazenado em um registro de máquina. Portanto, aqui simplesmente o XOR do registro é a instrução mais simples e barata para redefinir.

Então, da mesma maneira, uma transição incondicional, verificamos se algum evento externo ocorreu, verificamos a condição do ciclo, entramos no ciclo. O loop verifica se a soma é inteira: se sim, leia o valor inteiro, adicione o valor i, verifique se ocorreu um estouro, escreva o resultado de volta na soma e adicione 1 a %edx .

Pode-se ver que o código está próximo do ideal. Seria possível otimizá-lo ainda mais, eliminando a verificação da soma do tipo em cada iteração do loop. Mas isso já é uma otimização bastante complicada, ainda não o fazemos. Estamos desenvolvendo o JIT como uma tecnologia bastante simples , não estamos tentando fazer o que o Java HotSpot está tentando fazer, V8 - temos menos energia.

O que há de errado com jit


Por que, com um código tão bom de montador, não podemos acelerar aplicativos reais?

Na verdade, deveriam?

  • Se o gargalo não estiver na CPU, o JIT não ajudará.
  • Muito código é gerado (inchaço do código).
  • A inferência de tipo estático nem sempre funciona.
  • Código honesto (para casos que nunca são executados).
  • Suporte para o estado consistente da máquina virtual (e de repente uma exceção).
  • As aulas acontecem apenas para uma solicitação.

Se o aplicativo aguardar 80% do tempo por uma resposta do banco de dados, o JIT não ajudará. Se chamarmos funções externas que consomem muitos recursos, por exemplo, combinando com uma expressão regular, o JIT também chamará as mesmas funções da mesma maneira. Além disso, se um aplicativo cria grandes estruturas de dados - árvores, gráficos e as lê, em seguida, usando JIT, geramos código que lerá em menos instruções, mas para carregar os dados em si, levará o mesmo tempo, mas você também precisará carregar o código.

Como você já viu, o JIT pode até desacelerar um aplicativo real, porque gera muito código e a leitura se torna um problema - ao ler grandes quantidades de código, outros dados são forçados a sair do cache, o que leva a uma desaceleração.

Planos modestos para PHP 8


Uma das melhorias que queremos obter no PHP 8 é gerar menos código . Agora, como eu disse, geramos código nativo para todo o script, que carregamos no estágio de carregamento. Mas metade das funções certamente não será chamada. Então fomos um pouco mais longe e introduzimos um gatilho que nos permite configurar quando queremos executar o JIT. Pode ser executado:

  • para todas as funções;
  • somente para funções quando elas são chamadas pela primeira vez;
  • você pode pendurar um contador em cada função e compilar apenas as funções que são realmente boas.

Esse esquema pode funcionar um pouco melhor, mas ainda não é o ideal, porque novamente em cada função existem caminhos que são executados e caminhos que nunca são executados. Como o PHP é uma linguagem de programação dinâmica, ou seja, cada variável pode ter tipos diferentes, verifica-se que todos os tipos que o analisador estático prevê devem ser suportados. E ele costuma fazer isso com cautela quando não consegue provar que o outro tipo não pode fazê-lo.

Nessas condições, vamos nos afastar da compilação honesta e começar a fazê-lo especulativamente.



No futuro, planejamos, durante algum tempo, durante o trabalho de aplicação, analisar as funções mais "quentes", ver quais caminhos o programa percorre, quais tipos de variáveis ​​são, talvez até lembrar as condições de contorno e gerar apenas o código de função ideal para o atual maneira de execução - somente para as seções que são realmente executadas.

Para todo o resto, colocaremos stubs. Mesmo assim, haverá verificações e possíveis saídas nas quais o processo de desoptimização será iniciado, ou seja, restauraremos o estado da máquina virtual necessária para a interpretação e o forneceremos ao intérprete para execução.

Um esquema semelhante é usado no HotSpot Java VM e no V8. Mas adaptar a tecnologia ao PHP tem várias dificuldades. Primeiro de tudo, é que compartilhamos bytecode e código nativo compartilhado usados ​​em diferentes processos. Não podemos alterá-los diretamente na memória compartilhada, precisamos primeiro copiar em algum lugar, mudar e depois confirmar novamente na memória compartilhada.

Pré-carregamento. O problema da ligação de classe


De fato, muitas das idéias para aprimoramentos do PHP que há muito foram incluídas no PHP 7 e até no PHP 5 são provenientes de trabalhos relacionados ao JIT. Hoje vou falar sobre outra tecnologia desse tipo - isso é pré-carregamento. Essa tecnologia já está incluída no PHP 7.4 e permite especificar um conjunto de arquivos, carregá-los na inicialização do servidor e tornar todas as funções desses arquivos permanentes.

Um dos problemas que a tecnologia de pré-carregamento resolve é o problema da associação de classes. O fato é que, quando simplesmente compilamos arquivos em PHP, cada arquivo é compilado separadamente dos outros. Isso é feito porque cada um deles pode ser alterado separadamente. Você não pode associar uma classe de um script a uma classe de outro script, porque na próxima solicitação, um deles pode ser alterado e algo vai dar errado. Além disso, em vários arquivos, pode haver uma classe com o mesmo nome e, com uma solicitação, um deles é usado como pai e, com a outra, outra classe de outro arquivo é usada (com o mesmo nome, mas completamente diferente). Acontece que, ao gerar código que será executado em várias solicitações, você não pode se referir a classes ou métodos, porque eles são recriados toda vez (a vida útil do código excede a vida útil da classe).

O pré-carregamento permite vincular classes inicialmente e, consequentemente, gerar o código da melhor maneira possível. No mínimo, para estruturas que serão carregadas usando pré-carregamento.

Essa tecnologia ajuda não apenas na associação de classes. Algo semelhante é implementado em Java como Compartilhamento de dados de classe. Lá, essa tecnologia tem como objetivo principal acelerar o lançamento de aplicativos e reduzir a quantidade total de memória consumida. As mesmas vantagens são obtidas no PHP, porque agora a ligação de classe não é feita em tempo de execução, mas apenas uma vez. Além disso, as classes associadas agora são armazenadas não no espaço de endereço de cada processo, mas na memória compartilhada e, portanto, o consumo total de memória diminui.

O uso do pré-carregamento também ajuda na otimização global de todos os scripts PHP, remove completamente a sobrecarga do OPcache e permite gerar um código JIT mais eficiente.

Mas também há desvantagens. Os scripts carregados na inicialização não podem ser substituídos sem reiniciar o PHP. Se baixamos algo e o tornamos permanente, não podemos mais descarregá-lo. Portanto, a tecnologia pode ser usada com estruturas estáveis, mas se você implantar o aplicativo várias vezes ao dia, provavelmente não funcionará para você.

A tecnologia foi concebida como transparente, ou seja, permitiu carregar aplicativos existentes (ou partes deles) sem alterações. Mas, após a implementação, verificou-se que isso não é inteiramente verdade: nem todos os aplicativos funcionam como planejado se fossem carregados usando pré-carregamento . Por exemplo, se um código for chamado no aplicativo com base nos resultados da verificação de function_exists ou class_exists , e a função se tornar constante, respectivamente, function_exists sempre retornará true , e o código que foi chamado anteriormente foi pensado para não ser chamado.

Tecnicamente, o pré-carregamento é ativado com a ajuda de apenas uma diretiva de configuração opcache.preload, para a entrada da qual você fornece um arquivo de script - um arquivo PHP comum que será iniciado no estágio de inicialização do aplicativo (não apenas carregado, mas executado).

 <?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 é um dos cenários possíveis que lê recursivamente todos os arquivos em algum diretório (neste caso, ZendFramework). Você pode implementar absolutamente qualquer script no PHP: leia com uma lista, adicione exceções ou até cruze com o compositor para que ele possa remover arquivos necessários para o pré-carregamento. Tudo isso é uma questão de tecnologia, e mais interessante não é como enviar, mas o que enviar.

O que carregar no pré-carregamento


Eu tentei essa tecnologia no WordPress. Se você acabou de enviar todos os arquivos * .php, o WordPress deixará de funcionar por causa do recurso mencionado anteriormente: ele possui uma verificação function_exists, que sempre se torna verdadeira. Portanto, tive que modificar levemente o script do exemplo anterior (adicionar exceções) e, sem alterações no próprio WordPress, ele funcionou.

Velocidade [req / seq]Memória [MB]Número de scriptsNúmero de funçõesNúmero de aulas
Nada3780 00 00 00 0
Todos (quase *)3957,52541770148
Somente scripts usados3964,584153251

Como resultado, devido ao pré-carregamento, obtivemos ~ 5% de aceleração , o que já não é ruim.

Eu baixei quase todos os arquivos, mas metade deles não foram usados. Você pode fazer ainda melhor - conduza o aplicativo, veja quais arquivos foram baixados. Você pode fazer isso usando a função opcache_get_status() , que retornará todos os arquivos em cache do OPcache e criará uma lista para eles para pré-carregamento. Assim, você pode economizar 3 MB e obter um pouco mais de aceleração. O fato é que quanto mais memória é necessária, mais o cache do processador fica sujo e menos eficiente ele é. Quanto menos memória for usada, maior a velocidade.

FFI - Interface de Função Estrangeira


Outra tecnologia relacionada ao JIT que foi desenvolvida para PHP é a FFI (Foreign Function Interface) ou, em russo, a capacidade de chamar funções escritas em outras linguagens de programação compiladas sem compilação. As implementações dessa tecnologia em Python impressionaram meu chefe (Zeev Surazki), e fiquei muito impressionado quando comecei a adaptá-lo ao PHP.

Já houve várias tentativas no PHP de criar uma extensão para o FFI, mas todos usaram sua própria linguagem ou API para descrever as interfaces. Eu espiei a idéia no LuaJIT, onde a linguagem C (um subconjunto) é usada para descrever as interfaces, e o resultado é um brinquedo muito legal. Agora, quando preciso verificar como algo funciona em C, escrevo em PHP - isso acontece, diretamente na linha de comando.

O FFI permite trabalhar com estruturas de dados definidas em C e pode ser integrado ao JIT para gerar código mais eficiente. Sua implementação baseada em libffi já está incluída no PHP 7.4.

Mas:

  • Estas são 1000 novas maneiras de dar um tiro no próprio pé.
  • Requer conhecimento de C e, às vezes, gerenciamento manual de memória.
  • Não suporta pré-processador C (#include, #define, ...) e C ++.
  • O desempenho sem JIT é bastante baixo.

Embora, talvez para alguns seja conveniente, porque o compilador não é necessário. Mesmo no Windows, isso funcionará sem o Visual-C do PHP.

Vou mostrar como usar o FFI para implementar um aplicativo GUI real para Linux.

Não se assuste com o código C, eu mesmo escrevi uma GUI em C há 20 anos, mas encontrei esse exemplo na 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; } 

O programa cria o aplicativo, trava no evento de retorno de chamada de ativação, inicia o aplicativo. No retorno de chamada, crie uma janela, atribua o tamanho do título e mostre-o.

E agora, a mesma coisa reescrita no 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); 

Aqui, o objeto FFI é criado primeiro. Uma descrição da interface é enviada a ele como uma entrada - em essência, um arquivo h - e a biblioteca que queremos baixar. Depois disso, todas as funções descritas na interface tornam-se disponíveis como métodos do objeto ffi, e todos os parâmetros transferidos são automática e absolutamente transparentemente convertidos na representação necessária da máquina.

Pode-se ver que tudo é exatamente igual ao exemplo anterior. A única diferença é que, em C, enviamos um retorno de chamada como endereço e, no PHP, a conexão ocorre pelo nome fornecido pela string.

Agora vamos ver como é a interface. Na primeira parte, determinamos os tipos e funções em C e, na última linha, carregamos a biblioteca compartilhada:

 <?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"); ... 

Nesse caso, essas definições C são copiadas dos arquivos h da biblioteca GTK, quase inalteradas.

Para não interferir com C e PHP no mesmo arquivo, você pode colocar o código C inteiro em um arquivo separado, por exemplo, com o nome gtk-ffi.h e adicionar alguns define'ov especiais ao início que especificam o nome da interface e a biblioteca para carregamento:

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

Assim, selecionamos toda a descrição da interface C em um arquivo. Esse gtk-ffi.h é quase real, mas infelizmente ainda não implementamos um pré-processador C, o que significa que macros e inclusões não funcionarão.

Agora vamos carregar esta interface no 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); } } 

Como a FFI é uma tecnologia bastante perigosa, não queremos entregá-la às mãos de ninguém. Vamos pelo menos ocultar o objeto FFI, ou seja, torná-lo privado dentro da classe. E criaremos um objeto FFI não usando FFI::cdef , mas usando FFI::load , que lê apenas nosso arquivo h do exemplo anterior.

O restante do código não mudou muito, apenas como manipulador de eventos, começamos a usar uma função sem nome e passamos o título usando ligação lexical. Ou seja, usamos C e os pontos fortes do PHP, que não estão disponíveis em C.

Uma biblioteca criada dessa maneira já pode ser usada em seu aplicativo. Mas é bom que funcione apenas na linha de comando e, se você o colocar dentro do servidor da Web, em cada solicitação o arquivo gtk_ffi.h será lido, uma biblioteca será criada e carregada, com a ligação concluída ... E todo esse trabalho repetitivo carregará o servidor.

Para evitar isso e, de fato, permitir a gravação de extensões PHP no próprio PHP, decidimos cruzar o FFI com o pré-carregamento.

FFI + pré-carregamento


O código não mudou muito, só agora damos os arquivos h para o pré-carregamento e executamos o FFI::load diretamente no momento do pré-carregamento, e não quando criamos o objeto. Ou seja, ao carregar a biblioteca, todas as análises e ligações são feitas uma vez (quando o servidor é iniciado) e, usando FFI::scope("GTK") , obtemos acesso à interface pré-carregada pelo nome em nosso 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); } } 

Nesta modalidade, o FFI pode ser usado a partir de um servidor da web. Obviamente, isso não é para a GUI, mas dessa forma você pode escrever, por exemplo, uma ligação ao banco de dados.

Uma extensão criada dessa maneira pode ser usada diretamente na linha de comando:
 $ php -d opcache.preload=gtk.php -r 'GTK::create_window(" !");' 

Outra vantagem do cruzamento e pré-carregamento de FFI é a capacidade de proibir o uso de FFI para todos os scripts no nível do usuário. Você pode especificar ffi.enable = preload, o que significa que confiamos nos arquivos pré-carregados, mas é proibido chamar FFI a partir de scripts PHP regulares.

Trabalhando com estruturas de dados C


Outro recurso interessante do FFI é que ele pode trabalhar com estruturas de dados nativas. Você pode, a qualquer momento, criar na memória qualquer estrutura de dados descrita em 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/pt481092/


All Articles