PHP의 새로운 가능성: JIT와 수치 연산의 만남
웹 개발의 대명사로 불리던 PHP가 이제는 AI 추론 영역에서도 주목받고 있습니다. PHP 8.0부터 도입된 JIT(Just-In-Time) 컴파일은 단순한 성능 개선을 넘어, 언어의 활용 범위를 크게 확장했습니다. 특히 행렬 연산이 빈번한 머신러닝 추론 과정에서 JIT가 가져오는 이점은 상당합니다.
기존 Zend 엔진의 해석 실행 방식은 OPcode를 한 줄씩 읽어 처리하는 구조였습니다. 반면 JIT는 실행 중 자주 호출되는 코드 영역을 지해 네이티브 기계어로 변환합니다. 이로 인해 반복적인 수학 연산이 포함된 루프에서 수 배에서 수십 배의 성능 향상을 기대할 수 있습니다.
JIT 활성화 및 주요 설정
php.ini에서 다음과 같이 설정하면 JIT을 사용할 수 있습니다.
; JIT 버퍼 크기 및 최적화 레벨 지정
opcache.jit_buffer_size=128M
opcache.jit=tracing
위 설정에서 opcache.jit 값은 최적화 전략을 결정합니다. tracing 모드는 실행 경로를 추적하여 핫스팟을 식별하고, 이를 기계어로 변환합니다. 버퍼 크기는 컴파일된 기계어 코드를 저장하는 공간으로, 복잡한 연산을 수행할수록 넉넉히 할당하는 것이 좋습니다.
행렬 곱셈의 JIT 가속 효과
신경망의 핵심 연산인 행렬 곱셈을 PHP로 구현해보겠습니다. 다음 코드는 3×3 행렬 두 개를 곱하는 예제입니다.
<?php
function matMul(array $m1, array $m2): array
{
$dim = count($m1);
$output = array_fill(0, $dim, array_fill(0, $dim, 0));
for ($row = 0; $row < $dim; $row++) {
for ($col = 0; $col < $dim; $col++) {
$accum = 0.0;
for ($idx = 0; $idx < $dim; $idx++) {
$accum += $m1[$row][$idx] * $m2[$idx][$col];
}
$output[$row][$col] = $accum;
}
}
return $output;
}
$alpha = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
$beta = [[9.0, 8.0, 7.0], [6.0, 5.0, 4.0], [3.0, 2.0, 1.0]];
var_dump(matMul($alpha, $beta));
이 코드에서 삼중 루프는 JIT 컴파일러가 최적화하기 이상적인 패턴입니다. 반복 횟수가 충분히 많으면 JIT이 내부 루프를 기계어로 변환하여 해석 오버헤드를 제거합니다. 실제로 큰 차원의 행렬 연산에서는 JIT 미적용 대비 현저한 속도 차이를 보입니다.
PHP 8.5 JIT 아키텍처의 진화
PHP 8.5에서는 JIT 컴파일러가 타입 추론 능력을 대폭 강화했습니다. 이전 버전에서는 동적 타입으로 인해 최적화가 제한적이었던 부분을, 런타임 타입 피드백을 수집해 재컴파일하는 방식으로 극복했습니다.
타입 안정성 기반 재컴파일
PHP 8.5의 JIT은 함수 호출 시 수집된 타입 정보를 바탕으로, 타입이 일정 비율 이상 유지되는 경우 특수화된 기계어를 생성합니다. 이를 통해 다형성 함수도 효과적으로 최적화할 수 있습니다.
// JIT 재컴파일 조건을 보여주는 개념적 예시
function shouldRecompile(callCount, typeStability): bool {
return callCount > 5000 && typeStability >= 0.92;
}
위 조건을 만족하면 JIT 컴파일러는 해당 함수에 대해 고도로 최적화된 기계어 코드를 생성합니다. 반면 타입이 자주 바뀌는 함수는 과도한 재컴파일을 방지하기 위해 일정 수준의 최적화만 적용됩니다.
성능 지표 비교
| PHP 버전 | 요청 처리량(QPS) | 개선율 |
|---|---|---|
| 8.3 | 11,200 | - |
| 8.4 | 12,400 | 10.7% |
| 8.5 | 16,800 | 50.0% |
FFI를 통한 ONNX Runtime 연동
PHP에서 직접 딥러닝 모델을 실행하려면 외부 런타임과의 연결이 필요합니다. PHP 7.4부터 제공되는 FFI(Foreign Function Interface)를 활용하면 C 라이브러리를 직접 호출할 수 있습니다.
ONNX Runtime C API 바인딩
<?php
$onnxLib = FFI::cdef("
typedef struct OrtEnv OrtEnv;
typedef struct OrtSession OrtSession;
typedef struct OrtValue OrtValue;
int OrtCreateEnv(int log_level, const char* name, OrtEnv** env);
int OrtCreateSession(OrtEnv* env, const char* model_path, void* options, OrtSession** session);
int OrtRun(OrtSession* session, const char* const* names, OrtValue* const* inputs, size_t num_inputs,
const char* const* output_names, size_t num_outputs, OrtValue** outputs);
", "libonnxruntime.so");
$envPtr = $onnxLib->new("OrtEnv*");
$onnxLib->OrtCreateEnv(3, "php_inference", FFI::addr($envPtr));
$sessionPtr = $onnxLib->new("OrtSession*");
$onnxLib->OrtCreateSession($envPtr, "/path/to/model.onnx", null, FFI::addr($sessionPtr));
위 예제는 ONNX Runtime의 핵심 함수들을 FFI로 노출하는 방법을 보여줍니다. 실제 운영 환경에서는 입력 텐서를 OrtValue로 포장하고, 실행 후 결과를 해석하는 과정이 추가됩니다.
PHP 기반 경량 이미지 분류 서비스 구축
PHP의 강점인 웹 통합을 활용해, AI 추론 엔드포인트를 구축할 수 있습니다. 다음은 업로드된 이미지를 분류하는 간단한 서비스 예제입니다.
<?php
require 'vendor/autoload';
use GuzzleHttp\Client;
class ImageClassifier
{
private Client $httpClient;
private string $modelEndpoint;
public function __construct(string $endpoint)
{
$this->httpClient = new Client();
$this->modelEndpoint = $endpoint;
}
public function predict(string $imageData): array
{
$response = $this->httpClient->post($this->modelEndpoint, [
'headers' => ['Content-Type' => 'application/octet-stream'],
'body' => $imageData,
]);
$decoded = json_decode($response->getBody(), true);
return [
'category' => $decoded['top_class'],
'confidence' => $decoded['scores'][0],
];
}
}
// 실제 사용
$rawImage = file_get_contents('uploads/photo.jpg');
$classifier = new ImageClassifier('http://localhost:5000/infer');
$result = $classifier->predict($rawImage);
echo "예측 결과: {$result['category']} (신뢰도: {$result['confidence']})";
이 구조에서 실제 추론은 Python 기반의 전용 서버에서 수행되고, PHP는 요청 중계와 결과 가공을 담당합니다. 두 프로세스 간의 통신은 gRPC나 HTTP/2로 최적화할 수 있습니다.
PHP-ML을 활용한 클라이언트 사이드 머신러닝
외부 서비스 호출 없이 PHP 내부에서 간단한 ML 작업을 수행하려면 php-ml/php-ml 라이브러리를 고려할 수 있습니다. 이 패키지는 k-NN, Naive Bayes, SVM 등 기본 알고리즘을 순수 PHP로 구현합니다.
<?php
use Phpml\Classification\KNearestNeighbors;
use Phpml\Metric\Accuracy;
$samples = [[1, 3], [1, 4], [2, 4], [3, 1], [4, 1], [4, 2]];
$labels = ['a', 'a', 'a', 'b', 'b', 'b'];
$estimator = new KNearestNeighbors(3);
$estimator->train($samples, $labels);
$prediction = $estimator->predict([2, 2]);
echo "분류 결과: " . $prediction[0]; // 출력: a
물론 대규모 데이터셋이나 복잡한 신경망에는 적합하지 않지만, 사용자 행위 분류나 간단한 추천 로직 등 소규모 작업에서는 유용합니다. JIT이 활성화된 환경에서는 훈련과 예측 과정 모두에서 가속 효과를 얻을 수 있습니다.
수치 연산에서의 JIT 최적화 주의사항
JIT이 모든 상황에서 마법 같은 성능 향상을 가져오는 것은 아닙니다. 다음과 같은 패턴은 오히려 성능 저하를 초래할 수 있습니다.
- 루프 내부에서 예외를 throw하는 경우
- 변수의 타입이 반복문을 돌며 자주 변경되는 경우
- 동적으로 생성된 콜백이나 클로저를 호출하는 경우
특히 타입 불안정성은 JIT 컴파일러가 최적화된 기계어를 생성하는 것을 방해합니다. 수치 연산 코드에서는 변수의 타입을 일관되게 유지하는 것이 중요합니다.
<?php
// 비효율적: 루프 내에서 타입이 변경됨
function unstableSum(array $items): mixed
{
$total = 0;
foreach ($items as $item) {
$total += $item; // int일 수도, float일 수도 있음
if ($total > 1000) {
$total = "overflow"; // string으로 타입 변경!
}
}
return $total;
}
// 효율적: 타입을 일관되게 유지
function stableSum(array $items): float
{
$total = 0.0;
foreach ($items as $item) {
$total += (float) $item;
}
return $total;
}
PHP와 AI 생태계의 향후 전망
PHP가 Python이나 C++ 대비 AI 분야에서 경쟁력을 갖추기는 어렵습니다. 하지만 기존 PHP 기반 시스템에 AI 기능을 통합하려는 시나리오에서는 여러 장점이 있습니다.
| 통합 방식 | 적합한 시나리오 | 예상 지연 시간 |
|---|---|---|
| REST API 호출 | 클라우드 기반 모델 서빙 | 100-500ms |
| gRPC 스트리밍 | 실시간 데이터 파이프라인 | 20-100ms |
| FFI + ONNX Runtime | 엣지 서버 내 로컬 추론 | 5-50ms |
| 순수 PHP (php-ml) | 규칙 기반 분류, 소규모 데이터 | <10ms |
특히 기존 e-커머스 플랫폼이나 CMS에 제품 추천, 리뷰 감성 분석 등의 기능을 추가할 때, PHP 생태계 내에서 해결하는 방안은 운영 복잡도를 크게 줄여줍니다. JIT의 성능 향상은 이러한 접근을 더욱 현실적인 선택지로 만들고 있습니다.