0Pricing
PHP Academy · 강의

Zend Engine의 작동 원리

소스 코드에서 연산 코드, 실행에 이르기까지 PHP의 흐름을 추적합니다.

Zend Engine의 작동 원리은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

엔진 내부

PHP는 소스에서 한 줄씩 해석되지 않습니다. Zend 엔진은 사용자의 스크립트를 연산 코드라는 중간 표현으로 컴파일한 다음, 가상 머신이 이를 실행합니다. 이 처리 흐름을 이해하면 성능, OPcache, JIT가 어떻게 작동하는지 알 수 있습니다.

이 단원에서는 소스 → 토큰 → AST → 연산 코드 → VM 실행의 과정을 살펴봅니다.

처리 과정

각 PHP 파일에 대한 요청은 네 단계를 거칩니다.

  • 어휘 분석 — 소스 텍스트 → 토큰(re2c 기반 스캐너)
  • 구문 분석 — 토큰 → 추상 구문 트리(Bison 문법)
  • 컴파일 — AST → 연산 코드 배열(op_array)
  • 실행 — Zend VM이 연산 코드를 순회합니다

OPcache가 없으면 앞의 세 단계가 모든 요청마다 반복됩니다.

토큰화

어휘 분석기는 문자를 T_VARIABLE, T_ECHO, T_STRING 같은 토큰으로 변환합니다. PHP는 token_get_all() / PhpToken을 통해 이 단계를 제공하며, PHP-CS-Fixer 같은 도구가 바로 이를 사용합니다.

<?php
$src = '<?php $x = 1 + 2; echo $x;';

foreach (PhpToken::tokenize($src) as $tok) {
    if ($tok->isIgnorable()) continue;   // skip whitespace
    printf("%-12s %s\n", $tok->getTokenName(), trim($tok->text));
}
?>

AST

토큰은 노드로 이루어진 트리로 구문 분석됩니다. 예를 들어 대입 노드의 자식은 변수와 이항 연산 표현식이 됩니다. PHP는 내부적으로 이 AST를 만들며, nikic/php-parser는 PHPStan 같은 정적 분석 도구를 위해 사용자 영역에서 이에 상응하는 AST를 재구성합니다.

<?php
// Conceptual AST for:  $x = 1 + 2;
//
//   AST_ASSIGN
//   ├── AST_VAR  ($x)
//   └── AST_BINARY_OP (+)
//       ├── 1
//       └── 2
//
// At compile time PHP folds 1 + 2 into a literal 3
// (constant folding) before generating opcodes.
echo "AST drives opcode generation\n";
?>

연산 코드

컴파일러는 연산 코드로 이루어진 평평한 목록인 연산 코드 배열을 생성합니다. 각 연산 코드에는 연산 코드 번호(예: ZEND_ADD, ZEND_ECHO, ZEND_ASSIGN)와 최대 두 개의 피연산자 및 하나의 결과가 있으며, 각각은 컴파일된 변수(CV), 임시 값(TMP) 또는 상수입니다.

<?php
// Opcodes for:  $x = 1 + 2; echo $x;
//
// line  op            operands           result
//  ---  ------------  -----------------  -------
//   1   ADD           1, 2               ~0
//   1   ASSIGN        $x, ~0
//   1   ECHO          $x
//   1   RETURN        1
//
// ~0 is a temporary; $x is a compiled variable (CV).
echo "op_array is what OPcache stores\n";
?>

연산 코드 검사

VLD 확장 기능이나 OPcache의 opcache.opt_debug_level을 사용하면 생성된 연산 코드를 덤프할 수 있습니다. 이를 통해 상수 접기, 도달할 수 없는 코드 제거, 제어 흐름이 JMP/JMPZ 연산 코드로 바뀌는 방식을 확인할 수 있습니다.

# Dump opcodes with VLD
php -d vld.active=1 -d vld.execute=0 script.php

# Or via OPcache optimizer debug (pre/post optimization)
php -d opcache.opt_debug_level=0x10000 script.php   # before opt
php -d opcache.opt_debug_level=0x20000 script.php   # after opt

컴파일된 변수(CV)

로컬 변수는 컴파일된 코드에서 해시 테이블을 조회하지 않습니다. 컴파일러가 각 변수에 번호가 매겨진 CV 슬롯을 할당하기 때문입니다. 두 번째로 $x에 접근할 때는 심볼 테이블을 검색하는 대신 배열 인덱스를 사용합니다. 이것이 로컬 변수가 빠른 주요 이유입니다.

<?php
// Each named local gets a fixed CV slot at compile time:
//   $a -> CV0   $b -> CV1   $sum -> CV2
function add(int $a, int $b): int {
    $sum = $a + $b;   // ADD CV0, CV1 -> CV2
    return $sum;      // RETURN CV2
}
echo add(2, 3) . PHP_EOL;
?>

VM 실행 루프

실행기(execute_ex)는 연산 코드 배열을 순회합니다. 각 연산 코드는 하나의 처리기 함수에 대응하며, PHP는 이 디스패치를 거대한 분기문, 계산된 점프 또는 혼합 방식으로 구성할 수 있습니다. 혼합 방식은 기본값이며 이를 지원하는 컴파일러에서 가장 빠릅니다. 연산 코드 위치 포인터가 앞으로 이동하고, 제어 흐름이 바뀌면 JMP 연산 코드가 포인터를 이동시킵니다.

<?php
// if ($n > 0) echo 'pos'; compiles roughly to:
//
//   IS_SMALLER 0, $n  -> ~T
//   JMPZ ~T, ->L1        ; if false, skip
//   ECHO 'pos'
// L1:
//   RETURN 1
//
// The VM follows opline; JMPZ rewrites it conditionally.
$n = 5;
if ($n > 0) echo "pos\n";
?>

OPcache의 역할

OPcache는 컴파일된 연산 코드 배열을 공유 메모리에 캐시하여 이후 요청에서 어휘 분석, 구문 분석, 컴파일을 건너뜁니다. 또한 첫 번째 최적화 단계에서 최적화(상수 접기, 도달할 수 없는 코드 제거, 연산 코드 융합)를 수행합니다. VM은 요청마다 캐시된 연산 코드를 계속 실행하며, 바로 이 부분에서 나중에 JIT가 도움을 줍니다.

함수 호출 오버헤드

함수를 호출하면 새로운 VM 스택 프레임이 생성됩니다. 연산 코드 INIT_FCALL이 실행되고, 각 인수마다 SEND_VAL/SEND_VAR가 실행된 다음 DO_FCALL이 실행됩니다. 이 과정을 알면 지나치게 많은 작은 함수 호출에 측정 가능한 비용이 발생하는 이유와, 자주 실행되는 루프에서 인라인화/JIT가 중요한 이유를 이해할 수 있습니다.

<?php
// square($x) compiles to a call sequence:
//   INIT_FCALL 'square'
//   SEND_VAR   $x
//   DO_FCALL        -> ~R
//   ASSIGN $y, ~R
function square(int $x): int { return $x * $x; }

$total = 0;
for ($i = 1; $i <= 5; $i++) {
    $total += square($i);   // one call sequence per iteration
}
echo $total . PHP_EOL;     // 1+4+9+16+25 = 55
?>

요청 종료

실행이 끝나면 PHP는 요청을 정리합니다. 실행 중인 변수를 해제하고 출력을 비운 다음, 엔진이 할당자 영역을 초기화합니다. 공유하지 않는 FPM 모델에서는 각 요청이 깨끗한 상태로 시작하므로 한 요청의 치명적 오류가 다른 요청을 손상시킬 수 없습니다. OPcache의 공유 연산 코드 배열은 요청 간에 유지되지만 실행기 상태는 유지되지 않습니다.

빠른 확인

재컴파일을 건너뛰기 위해 OPcache가 정확히 저장하는 것은 무엇인가요?

복습

Zend의 처리 과정은 어휘 분석 → 구문 분석 → 컴파일 → 실행입니다. 소스는 토큰이 되고, 이어서 AST가 된 다음 VM의 실행 루프가 실행하는 연산 코드 배열이 됩니다. 로컬 변수는 번호가 매겨진 CV 슬롯에 저장되고, 함수 호출은 스택 프레임을 생성합니다. OPcache는 연산 코드 배열과 최적화 결과를 캐시하므로 요청마다 실행만 반복됩니다.

자주 묻는 질문

“Zend Engine의 작동 원리” 강의는 무료인가요?

네 — “Zend Engine의 작동 원리” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“Zend Engine의 작동 원리”에서 뭘 배우나요?

소스 코드에서 연산 코드, 실행에 이르기까지 PHP의 흐름을 추적합니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

PHP Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“Zend Engine의 작동 원리” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Zend Engine의 작동 원리
  2. 메모리 관리와 가비지 컬렉션
  3. OPcache와 JIT 컴파일
  4. C로 기본 PHP 확장 기능 작성
← PHP Academy(으)로 돌아가기