Provavelmente, se você acompanha algum canal sobre alguma linguagem de programação deve ter visto recentemente o “1 Billion nested loop iterations”, uma publicação que busca mostrar o tempo de resposta de algumas linguagens de programação ao percorrer esses 1 bilhão de dados.
Algo bastante interessante, e que faz nós, como programadores, pensar em maneiras de melhorar a performance da nossa linguagem, e é isso que quero falar hoje. Quero apresentar para vocês o amigo dos devs PHP quando o assunto é dar aquele up na performance de grandes aplicações, estou falando do OPCache e do JIT que são funcionalidades que tem como objetivo melhorar o desempenho na execução de scripts PHP.
OPCache
Começando pela base, o OPCache é uma extensão para o PHP que melhora o desempenho das aplicações ao armazenar o bytecode dos scripts PHP em um cache na memória compartilhada do servidor. Apesar de ter ganhado força ultimamente, principalmente por conta das versões 8.* do PHP, o OPCache foi introduzido inicialmente como parte do PHP na versão 5.5, porém, ele era uma extensão separada conhecida como “Zend Optimizer+”.
Como o compilador do PHP funciona?
Por padrão, quando o processo de execução de um script PHP é iniciado, o servidor vai ler o arquivo .php do disco, e em seguida iniciar o “Parsing e Tokenização” que basicamente, pega o código PHP, análise ele e o transforma em tokens. Em seguida, começa a etapa de compilação desses tokens, que são convertidos em bytecode, que são uma representação intermediária (entre o código-fonte PHP e o código de máquina executado pela CPU) otimizada do código PHP, esse bytecode é o que vai ser executado pela Zend Engine (engine padrão do PHP).
Esse processo por sua vez, é executado toda vez que um script é chamado, mesmo que o código-fonte do script não tenha mudado nada, o que, por sua vez, consome recursos do servidor, especialmente em aplicações que envolvem muitos scripts PHP.
Certo, e como o OPCache otimiza esse processo?
Chegamos no OPCache, basicamente, ele intervém nesse processo padrão armazenando o bytecode compilado do seu script PHP na memória compartilhada, só que, na primeira execução do seu script, o PHP vai seguir essa etapa padrão, gerando o bytecode, e o OPCache armazenando ele, contudo, nas próximas execuções o PHP irá verificar se o script já tem um bytecode armazenado em cache, e caso ele exista, ele é carregado diretamente da memória, evitando o processo repetido.
Como benefícios, o OPCache traz consigo uma grande diminuição no tempo de execução do seu projeto, já que o bytecode será buscado diretamente da memória. O que por sua vez, diminui o consumo da CPU, já que o processo de parsing e compilação são evitadas durante esse processo, e claro, trazendo uma consistência maior para o seu código, já que os scripts cacheados são executados de forma consistente, independentemente de variaçÕes no desempenho do sistema de arquivos.
Então, como faço para ativar o OPCache?
Para ativá-lo é bem simples, você precisa apenas ir no seu arquivo php.ini e configurar as seguintes linhas:
; Ativa o OPCache
opcache.enable=1
; Tamanho máximo da memória compartilhada alocada para o cache (em MB)
opcache.memory_consumption=128
; Quantidade de memória reservada para strings internadas (em MB)
opcache.interned_strings_buffer=8
; Número máximo de arquivos que podem ser armazenados no cache
opcache.max_accelerated_files=10000
; Valida se o script foi alterado antes de executar (1 para ambiente de desenvolvimento)
opcache.validate_timestamps=1
; Intervalo em segundos para verificar alterações nos scripts (0 para produção)
opcache.revalidate_freq=2
OBS: Em ambientes de produção, é recomendado desativar a validação constante de scripts para maior desempenho:
opcache.validate_timestamps=0
opcache.revalidate_freq=0
Um ponto que você deve ficar alerta nessas configurações, é caso seus scripts tenham uma atualização recorrente, pois caso um script PHP for modiciado, ele não será atualizado no cache até que o OPCache o detecte (baseado em validade_timestamps), ou caso você limpe o cache manualmente.
Agora que você entende o que é o OPCache, o que é o JIT?
Apesar do nome, não, não estamos falando do sistema de administração Just-in-Time, porém, ambos seguem o mesmo princípio, acelerar processos.
O JIT (Just-In-Time compilation) é uma técnica de compilação dinâmica que converte o bytecode PHP diretamente em código de máquina nativo (assembly), o que é diferente do que foi citado anteriormente, já que o PHP interpreta o bytecode gerado a partir do código-fonte. Isso permite que o seu código seja executado diretamente pela CPU, sem a necessidade de interpretação contínua pela Zend Engine. Essa abordagem também pode ser vista em outras linguagens modernas, como o JavaScript (V8), Java (JVM) e até mesmo o Python (PyPy), para melhorar o desempenho.
Certo, e como o JIT funciona no PHP?
Basicamente, sem o PHP no seu fluxo tradicional sem o JIT, converte o código-fonte do seu script em bytecode pelo compilador da Zend Engine (como falamos anteriormente), e esse bytecode é interpretado pela Zend Engine, instrução por instrução.
Com o JIT o jogo muda, o bytecode é analisado durante a execução, e partes do código são que são executadas frequentemente (como loops ou cálculos intensivos) são compiladas para código de máquina nativo, que é executado diretamente pela CPU, sem precisar passar pela Zend Engine.
O JIT no PHP funciona em conjunto com o OPCache, que já armazena o bytecode em cache. Enquanto o OPCache melhora a reutilização do bytecode, o JIT vai além, gerando código de máquina otimizado.
Como configurar o JIT?
O JIT possui várias maneiras de ser configurado através da opção opcache.jit no seu php.ini. Os principais modos incluem:
- Desativado (opcache.jit=0): O PHP funciona sem o JIT, usando apenas o OPCache para armazenar o bytecode.
- Tracing JIT (opcache.jit=1255): É o modo mais avançado e recomendado. Ele rastreia partes do código frequentemente executadas (hot paths) e as otimiza gerando código nativo.
- Function JIT (opcache.jit=1205): Compila funções inteiras para código nativo.
- Compile Time JIT: Compila todo o bytecode em código nativo logo na inicialização, mas geralmente não é usado devido ao alto custo inicial.
Vale se atentar que o JIT acaba se limitando para aplicações web tradicionais. A maioria dos aplicativos PHP (baseados em frameworks como Laravel ou até mesmo CMS como WordPress) não experimenta grandes ganhos de desempenho com o JIT. Isso ocorre justamente porque esses sistemas geralmente passam mais tempo em I/O (banco de dados e redes) do que em execução intensiva de CPU. Portanto, caso você não vá usar intensamente a CPU durante a execução dos seus scripts, não tem a necessidade de habilitar o JIT. Um bom uso seria em scripts que realizam cálculos matemáticos intensivos, processamento de imagens ou vídeo ou casos mais extremos como simulações científicas ou processamento de dados em tempo real.
A configuração no seu php.ini ficaria assim, por exemplo:
; Ativa o OPCache e define o buffer para o JIT
opcache.enable=1
opcache.jit_buffer_size=100M
; Configura o modo do JIT (Tracing JIT, recomendado)
opcache.jit=1255
Melhorando a execução do nosso script
Já que iniciei mostrando o “1 Billion nested loop iterations”, nada melhor que darmos um up a execução do nosso script. Ali estamos com 9.93s de execução do PHP, não sei exatamente como realizaram esse teste, mas estou supondo que rodaram apenas um for de 1 até 1 bilhão. Minhas configurações ficaram assim:
Estrutura das pastas
project/
├── docker-compose.yml
├── Dockerfile
├── php/
│ ├── index.php
│ └── php.ini
docker-compose.yml
version: '3.8'
services:
php:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./php:/var/www/html:ro
ports:
- "8080:80"
Dockerfile
# Base image com PHP 8.4 e Apache
FROM php:8.4-apache
# Instalação de pacotes adicionais e o OPCache
RUN docker-php-ext-install opcache
# Copia o php.ini para o diretório correto de configurações
COPY ./php/php.ini /usr/local/etc/php/conf.d/30-custom.ini
# Ativar o JIT e configurar o OPCache no php.ini
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/30-opcache.ini
RUN echo "opcache.jit_buffer_size=100M" >> /usr/local/etc/php/conf.d/30-opcache.ini
RUN echo "opcache.jit=1255" >> /usr/local/etc/php/conf.d/30-opcache.ini
# Configuração básica do Apache para servir o PHP
RUN a2enmod rewrite
index.php
<?php
$inicio = microtime(true);
for ($i = 1; $i <= 1000000000; $i++) {
}
$fim = microtime(true);
$tempoExecucao = $fim - $inicio;
echo "O tempo de execução foi de " . number_format($tempoExecucao, 4) . " segundos.";
php.ini
; Configurações básicas do PHP
display_errors=On
error_reporting=E_ALL
; Configurações do OPCache
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.jit_buffer_size=100M
opcache.jit=1255
Por fim, basta rodar o docker-compose up –build para subir seu projeto e acessar http://localhost:8080/index.php no seu navegador. Aqui obtive um ganho até que considerável de 86.16%, caimos de 9.93s para 1.37s
Por fim, fique à vontade para testar as configurações nesse projeto, às vezes você consegue até um nº melhor na sua máquina.