벤치마크
Phasis는 디스패치에 병목이 있는 JavaScript에서 V8보다 대략 100배 느립니다. 이는 PHP 기반 인터프리터와 네이티브 JIT 컴파일러 사이에 내재한 성능 차이입니다. 임베더가 물어야 할 것은 "V8와 비교하면 어떤가?"가 아니라 "내 작업에 충분히 빠른가?"입니다. 대부분의 임베딩 워크로드에서는 충분히 빠릅니다.
성능 한도가 문제가 되지 않는 경우
- 템플릿 처리 — 일반적인 페이지를 SSR 방식으로 렌더링하는 데 10 ms도 걸리지 않습니다.
- 검증 규칙 — 폼 검증용 사용자 제공 JS를 마이크로초 단위로 처리합니다.
- 데이터 변환 — 수백 개 항목에 대한
array.map(transform)를 수십 밀리초 안에 완료합니다. - 샌드박스 실행기 — 호스트가 1초 제한을 두는 짧은 사용자 스크립트도 충분한 여유를 둡니다.
성능 한도가 문제가 되는 경우
- 계산량이 많은 내부 루프 — 이미지 처리, 대규모 배열 축소, 시뮬레이션 등입니다. 계산을 네이티브 PHP 라이브러리로 옮기고 호스트 함수로 노출하세요.
- 실시간 애니메이션 — 60 fps를 유지해야 하는 작업입니다. 이런 JS는 임베디드 엔진이 아니라 클라이언트(브라우저, 네이티브 앱)에서 처리해야 합니다.
- 계산량이 많은 알고리즘을 JS로 재구현하는 경우 — 암호화, 압축, 파싱 등이 해당합니다. PHP에는 1000배 더 빠른 네이티브 구현이 있으므로 이를 사용하세요.
올바른 관점은 다음과 같습니다. Phasis는 PHP 형태의 애플리케이션에 임베드된 JS 형태의 로직을 위한 접착 계층입니다. PHP 호스트는 계산을 처리하고, JS 계층은 구성, 분기, 간단한 변환을 처리합니다.
순진한 트리 워커보다 엔진이 빠른 이유
- 바이트코드 VM — 대부분의 핫 디스패치는 AST가 아니라
VM::execute의 opcode 스위치를 통해 실행됩니다. - 사용자 지정 호출 스택 — 모든 JS 호출은 상태를
VM::execute안에 유지하고 호출자 프레임을 풀에 저장합니다. JS 호출마다 새로운 PHP 프레임을 만들지 않습니다. 바이트코드 VM § 사용자 지정 호출 스택을 참조하세요. - 인라인 캐시 — PC별 단형 캐시는 프로토타입 체인을 순회하지 않고 인스턴스의 프로토타입 메서드 읽기(
c.method()형태)를 처리합니다. - 프레임 스냅샷 제너레이터 — 간단한 제너레이터 본문은 PHP Fiber를 거치는 대신 디스패치 상태를 힙 객체에 캡처해 일시 중단합니다.
- JsToPhp 트랜스파일러 — 숫자 연산 또는 로컬 변수 사용이 많은 본문(
obj.method(),new Ctor(),this.member읽기 포함)은 PHP 클로저로 컴파일되며, PHP 8.5의 추적 JIT가 네이티브 기계어로 변환합니다. - JSON.stringify 캐시 —
JSON.stringify(o)는 전이적 버전 무효화를 적용해 입력 객체에 출력을 캐시하므로, 딥 클론 관용구인JSON.parse(JSON.stringify(o))의 속도를 높입니다.
직접 워크로드 측정하기
php bench/run.php
이는 bench/microbench.js에 포함된 마이크로벤치마크 모음을 실행합니다. 루프 산술, 재귀 함수, 객체 생성, 프로토타입 메서드 호출, 배열 push, JSON 왕복 등 몇 가지 합성 워크로드를 수행하고 테스트별 중앙값 벽시계 시간을 출력합니다. 기본 동작을 확인할 때 유용하며, 현재 수치는 모든 CI 벤치마크 실행 때마다 BENCH.md에 커밋됩니다.
직접 작성한 코드에는 아무 스크립트나 넣으세요:
php -d xdebug.mode=off bench/run.php path/to/your-script.js
PHP 수준의 프로파일링에는 Xdebug의 프로파일러를 사용할 수 있습니다. 다른 PHP 애플리케이션과 마찬가지로 Phasis에서 작동합니다. xdebug.mode=profile에서 스크립트를 실행한 뒤 cachegrind 출력을 살펴 어떤 Phasis\ 클래스가 주된 비용을 차지하는지 확인하세요.
로드맵
실현 가능성이 높은 순서대로, V8 격차를 더 줄일 수 있는 작업은 다음과 같습니다:
- 다형성 인라인 캐시 — 하나의
LOAD_MEMBER호출 지점에 느린 디스패치로 전환하기 전까지 캐시된 셰이프를 2-4개 저장합니다. 서로 다른 리시버를 처리하는 코드에 도움이 됩니다. yield*및 프레임 스냅샷 생성기 경로의 try-with-yield — 현재 이 셰이프는 Fiber 기반 트리 워커로 전달됩니다. 스냅샷을 지원하려면 각각 명시적인 상태 머신 처리(위임된 이터레이터 전달, suspend 중 핸들러 스택 보존)가 필요합니다.- 스냅샷의 비동기 생성기 및
await— 사양은 생성기 중단과 마이크로태스크 큐를 함께 구성합니다. 바이트코드 본문이yield와await를 모두 지원하면 비동기 생성기도 스냅샷을 지원할 수 있습니다. - JsToPhp의 매개변수 타입 추론 — 함수 매개변수는 기본적으로 숫자 타입이므로
function (obj) { obj.method() }에서 컴파일러가 중단됩니다. 본문에서의 사용 방식을 바탕으로 객체 타입 매개변수를 추론하면,new를 통해 리시버를 함수 내부에서 직접 생성하지 않아도 함수 인수의 메서드를 호출할 수 있습니다. - 직접 opcache 출력 — opcache 형식의 opcode를 직접 작성해 JsToPhp의
eval()단계를 건너뜁니다. 가장 난도가 높은 작업이며, 나머지는 기존 아키텍처 안에서 구현할 수 있습니다.
이러한 작업은 Phasis이 대상으로 삼는 임베딩 워크로드에 필요하지 않습니다.