Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ demo
*.reproduce.c
*.debug.c
.phpunit.result.cache
.php-compiler-cache
.php_cs.cache
.env
.php-compiler-ci.lock
2 changes: 1 addition & 1 deletion bin/jit.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function run(string $filename, string $code, array $options): void
// JIT EH IR may verify (TryCatchJitCompileTest); bin/jit.php VM-fallbacks EH/finally (#2114).
// Script-scope yield still uses VM; nested generator bodies use MCJIT resume (#3074, #3115).
} else {
$runtime->jit($block);
$runtime->jit($block, $code, $filename);
}

if (! isset($options['-l'])) {
Expand Down
2 changes: 1 addition & 1 deletion bin/serve-jit.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@

$mode = 'jit';
try {
$runtime->jit($block);
$runtime->jit($block, $code, $script);
} catch (\Throwable $e) {
$mode = 'vm';
fwrite(
Expand Down
303 changes: 303 additions & 0 deletions lib/JIT/CompileCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\JIT;

use PHPCompiler\Block;

/**
* On-disk MCJIT bitcode cache (issue #153).
*
* Persists verified LLVM bitcode keyed by source bytes + compiler fingerprint so a
* second `bin/jit.php` process can skip LLVM IR lowering when inputs are unchanged.
*/
final class CompileCache
{
private const META_VERSION = 1;

/** @var list<array{llvm: string, signature: string, scoped: string}>|null */
private static ?array $recordingExports = null;

private static ?string $recordingKey = null;

private static bool $skipModuleFuncCompile = false;

public static function isEnabled(): bool
{
$flag = getenv('PHP_COMPILER_CACHE');
if (false !== $flag && ('0' === $flag || 'false' === strtolower($flag))) {
return false;
}
if (getenv('PHP_COMPILER_SELFHOST_AOT') === '1') {
return false;
}
if (EmitTuMode::isMinimalRuntime()) {
return false;
}

return true;
}

public static function shouldSkipModuleFuncCompile(): bool
{
return self::$skipModuleFuncCompile;
}

public static function cacheRoot(): string
{
$override = getenv('PHP_COMPILER_CACHE_DIR');
if (is_string($override) && '' !== $override) {
return rtrim($override, '/');
}

return dirname(__DIR__, 2).'/.php-compiler-cache';
}

public static function computeKey(string $sourcePath, string $sourceCode): string
{
$resolved = realpath($sourcePath);
$pathPart = false !== $resolved ? $resolved : $sourcePath;
$mtime = is_file($pathPart) ? (string) filemtime($pathPart) : '0';

return hash('sha256', implode("\0", [
$pathPart,
$mtime,
strlen($sourceCode),
hash('sha256', $sourceCode),
self::fingerprint(),
]));
}

public static function entryDir(string $key): string
{
return self::cacheRoot().'/'.$key;
}

public static function bitcodePath(string $key): string
{
return self::entryDir($key).'/module.bc';
}

public static function metaPath(string $key): string
{
return self::entryDir($key).'/meta.json';
}

/**
* @return array{version: int, fingerprint: string, exports: list<array{llvm: string, signature: string, scoped: string}>}|null
*/
public static function readMeta(string $key): ?array
{
$path = self::metaPath($key);
if (!is_file($path)) {
return null;
}
$raw = file_get_contents($path);
if (false === $raw) {
return null;
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return null;
}
if ((int) ($decoded['version'] ?? 0) !== self::META_VERSION) {
return null;
}
if (($decoded['fingerprint'] ?? '') !== self::fingerprint()) {
return null;
}
if (!isset($decoded['exports']) || !is_array($decoded['exports'])) {
return null;
}

return $decoded;
}

public static function isFresh(string $key, string $sourcePath, string $sourceCode): bool
{
if (!self::isEnabled()) {
return false;
}
if (self::computeKey($sourcePath, $sourceCode) !== $key) {
return false;
}
if (!is_file(self::bitcodePath($key))) {
return false;
}

return null !== self::readMeta($key);
}

public static function beginRecording(string $key): void
{
self::$recordingKey = $key;
self::$recordingExports = [];
}

public static function recordExport(string $llvmName, string $signature, Block $block): void
{
if (null === self::$recordingExports) {
return;
}
self::$recordingExports[] = [
'llvm' => $llvmName,
'signature' => $signature,
'scoped' => self::blockScopedName($block),
];
}

/**
* @return bool true when bitcode was loaded and exports restored
*/
public static function tryRestore(Context $context, Block $block, string $key): bool
{
$meta = self::readMeta($key);
if (null === $meta) {
return false;
}
$bcPath = self::bitcodePath($key);
if (!is_file($bcPath)) {
return false;
}

try {
$context->replaceModuleFromBitcodeFile($bcPath);
} catch (\Throwable $e) {
return false;
}

self::restoreExports($context, $block, $meta['exports']);
self::$skipModuleFuncCompile = true;

return true;
}

public static function save(Context $context, string $key): void
{
if (null === self::$recordingExports) {
return;
}
$dir = self::entryDir($key);
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
return;
}

$lockPath = $dir.'/.lock';
$lock = @fopen($lockPath, 'c+');
if (false === $lock) {
return;
}
if (!flock($lock, LOCK_EX)) {
fclose($lock);

return;
}

try {
$context->module->writeBitcodeToFile(self::bitcodePath($key));
$payload = json_encode([
'version' => self::META_VERSION,
'fingerprint' => self::fingerprint(),
'exports' => self::$recordingExports,
], JSON_PRETTY_PRINT);
if (false !== $payload) {
file_put_contents(self::metaPath($key), $payload);
}
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
}

public static function finishRecording(): void
{
self::$recordingKey = null;
self::$recordingExports = null;
self::$skipModuleFuncCompile = false;
}

/**
* @param list<array{llvm?: string, signature?: string, scoped?: string}> $exports
*/
private static function restoreExports(Context $context, Block $block, array $exports): void
{
$blocksByScoped = self::collectBlocksByScopedName($block);
foreach ($exports as $entry) {
$llvm = $entry['llvm'] ?? '';
$signature = $entry['signature'] ?? '';
$scoped = $entry['scoped'] ?? '';
if ('' === $llvm || '' === $signature || '' === $scoped) {
continue;
}
if (!isset($blocksByScoped[$scoped])) {
continue;
}
$context->addExport($llvm, $signature, $blocksByScoped[$scoped]);
}
}

/**
* @return array<string, Block>
*/
private static function collectBlocksByScopedName(Block $root): array
{
$map = [];
$queue = [$root];
while ([] !== $queue) {
$current = array_shift($queue);
if (null !== $current->func) {
$map[$current->func->getScopedName()] = $current;
} else {
$map['{main}'] = $current;
}
foreach ($current->blocks as $child) {
$queue[] = $child;
}
}

return $map;
}

private static function blockScopedName(Block $block): string
{
if (null !== $block->func) {
return $block->func->getScopedName();
}

return '{main}';
}

private static function fingerprint(): string
{
static $cached = null;
if (null !== $cached) {
return $cached;
}

$parts = [];
$lock = dirname(__DIR__, 2).'/composer.lock';
if (is_file($lock)) {
$parts[] = hash_file('sha256', $lock) ?: '';
}
$repoRoot = dirname(__DIR__, 2);
$llvmDir = getenv('PHP_COMPILER_LLVM_PATH');
if (false === $llvmDir || '' === $llvmDir) {
$candidate = $repoRoot.'/.llvm';
if (is_file($candidate.'/libLLVM-9.so.1')) {
$llvmDir = $candidate;
} elseif (is_file('/opt/llvm9/libLLVM-9.so.1')) {
$llvmDir = '/opt/llvm9';
} else {
$llvmDir = 'no-llvm';
}
}
$parts[] = $llvmDir;
$parts[] = hash_file('sha256', __DIR__.'/../JIT/Context.php') ?: '';
$parts[] = hash_file('sha256', __DIR__.'/../Runtime.php') ?: '';

$cached = hash('sha256', implode("\0", $parts));

return $cached;
}
}
36 changes: 36 additions & 0 deletions lib/JIT/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ public function setMain(PHPLLVM\Value\Function_ $func): void {

public function addExport(string $name, string $signature, Block $block): void {
$this->exports[] = [$name, $signature, $block];
CompileCache::recordExport($name, $signature, $block);
}

/** Implicit $this passed as the first LLVM arg for instance methods (#877). */
Expand Down Expand Up @@ -595,6 +596,41 @@ public function compileInPlace() {
}
}

/** MCJIT from on-disk bitcode cache (#153). */
public function compileInPlaceFromDiskCache(): void {
if (!is_null($this->result)) {
return;
}
McjitEmbedRuntime::prepareModule($this);
$message = '';
$this->module->verify($this->module::VERIFY_ACTION_THROW, $message);
$engine = $this->module->createJITCompiler(0);
$this->result = new Result(
$engine,
$this->loadType
);
Builtin\ReadonlyRaise::bindJitEngine($engine);
Builtin\TypeErrorRaise::bindJitEngine($engine);
Builtin\JitThrow::bindJitEngine($engine);
foreach ($this->exports as $export) {
$export[2]->handler = $this->result->getHandler($export[0], $export[1]);
}
}

public function replaceModuleFromBitcodeFile(string $path): void {
$message = '';
$buffer = $this->llvm->createMemoryBufferWithFile($path, $message);
if ('' !== $message) {
throw new \RuntimeException('Bitcode read failed: '.$message);
}
try {
$this->module = $buffer->parseBitcode($this->context);
} finally {
$buffer->dispose();
}
$this->targetData = $this->module->getModuleDataLayout();
}

private function compileCommon() {
Progress::noteFunction('jit_context_compile_common_phase_modules_shutdown');
foreach ($this->modules as $module) {
Expand Down
Loading