refactor(runtime): optimize request cache storage with single TLS pointer

- Replace multiple TLS arrays with single php_request_cache_storage struct
- Use one TLS pointer instead of multiple static THREAD_LOCAL arrays
- Implement new/delete allocation for request-scoped cache storage
- Remove memset calls in favor of proper object lifecycle management
- Add proper initialization checks and error handling in RINIT
- Update all cache access patterns to use php_request_cache pointer
- Move persistent caches outside of TLS to static storage
- Include <new> header for allocation operations
- Update tests to verify new cache storage structure and behavior
master
韩天峰 1 day ago
parent 90df0a93d5
commit 48c95e9f9b
  1. 2
      .github/actions/unix-arm64-build/action.yml
  2. 2
      .github/workflows/linux-x64.yml
  3. 2
      .gitignore
  4. 20
      phpunit/src/CompilerBaseApiTest.php
  5. 56
      phpunit/src/PropertyCacheCodegenTest.php
  6. 72
      src/Translator.php

@ -30,7 +30,7 @@ runs:
php-version: ${{ inputs.php-version }} php-version: ${{ inputs.php-version }}
coverage: none coverage: none
extensions: mbstring extensions: mbstring
ini-values: opcache.jit=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 ini-values: precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0
tools: composer:v2 tools: composer:v2
env: env:
fail-fast: true fail-fast: true

@ -364,7 +364,7 @@ jobs:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
coverage: none coverage: none
extensions: curl, redis, mbstring, ffi extensions: curl, redis, mbstring, ffi
ini-values: ffi.enable=1, phpy.enable_operator_overloading=0, opcache.jit=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 ini-values: ffi.enable=1, phpy.enable_operator_overloading=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0
tools: composer:v2 tools: composer:v2
env: env:
fail-fast: true fail-fast: true

2
.gitignore vendored

@ -10,7 +10,6 @@
/projects /projects
/.php-cs-fixer.cache /.php-cs-fixer.cache
/.phpunit.cache /.phpunit.cache
/__pycache__
/composer.lock /composer.lock
*.o *.o
*.dll *.dll
@ -23,6 +22,7 @@
*.exp *.exp
*.class *.class
*.wasm *.wasm
__pycache__
/tpc /tpc
tests/**/*.diff tests/**/*.diff
tests/**/*.exp tests/**/*.exp

@ -1213,7 +1213,7 @@ YAML);
$this->assertContains('/usr/local/lib', $libraryPaths); $this->assertContains('/usr/local/lib', $libraryPaths);
} }
public function testRequestShutdownClearsRuntimeMapsInEveryBuildMode(): void public function testRequestLifecycleOwnsRuntimeCacheStorageInEveryBuildMode(): void
{ {
global $translator; global $translator;
foreach ([CompilerBase::BUILD_MODE_BIN, CompilerBase::BUILD_MODE_LIB, CompilerBase::BUILD_MODE_EXT] as $mode) { foreach ([CompilerBase::BUILD_MODE_BIN, CompilerBase::BUILD_MODE_LIB, CompilerBase::BUILD_MODE_EXT] as $mode) {
@ -1227,21 +1227,24 @@ YAML);
$compiler->convertFile($testFile); $compiler->convertFile($testFile);
$code = file_get_contents($compiler->genExtension()); $code = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('#include <cstring>', $code, $mode); $this->assertStringContainsString('struct php_request_cache_storage final {', $code, $mode);
$this->assertStringContainsString( $this->assertStringContainsString(
'std::memset(php_func_map, 0, sizeof(php_func_map));', 'static THREAD_LOCAL php_request_cache_storage *php_request_cache = nullptr;',
$code, $code,
$mode, $mode,
); );
$this->assertStringContainsString( $this->assertStringContainsString(
'std::memset(php_class_map, 0, sizeof(php_class_map));', 'php_request_cache = new (std::nothrow) php_request_cache_storage{};',
$code, $code,
$mode, $mode,
); );
$this->assertStringContainsString('delete php_request_cache;', $code, $mode);
$this->assertStringContainsString('php_request_cache = nullptr;', $code, $mode);
$this->assertStringNotContainsString('std::memset(php_func_map', $code, $mode);
$this->assertStringNotContainsString('std::memset(php_class_map', $code, $mode);
$this->assertStringNotContainsString('static THREAD_LOCAL zend_function *php_func_map[', $code, $mode);
$this->assertStringNotContainsString('static THREAD_LOCAL zend_class_entry *php_class_map[', $code, $mode);
$this->assertStringNotContainsString('php_property_map', $code, $mode); $this->assertStringNotContainsString('php_property_map', $code, $mode);
$this->assertStringNotContainsString('func_map = {}', $code, $mode);
$this->assertStringNotContainsString('class_map = {}', $code, $mode);
$this->assertStringNotContainsString('property_map = {}', $code, $mode);
} }
} }
@ -1284,7 +1287,8 @@ YAML);
$this->assertStringNotContainsString('slot.reset()', $moduleInit, $mode); $this->assertStringNotContainsString('slot.reset()', $moduleInit, $mode);
$this->assertMatchesRegularExpression( $this->assertMatchesRegularExpression(
'/PHP_RSHUTDOWN_FUNCTION\([^)]*\)\s*\{\s*' '/PHP_RSHUTDOWN_FUNCTION\([^)]*\)\s*\{\s*'
. 'for \(auto &slot : php_property_cache_map\) \{\s*slot\.reset\(\);\s*\}\s*' . 'delete php_request_cache;\s*'
. 'php_request_cache = nullptr;\s*'
. 'php::request_shutdown\(\);/s', . 'php::request_shutdown\(\);/s',
$extension, $extension,
$mode, $mode,

@ -1,9 +1,65 @@
<?php <?php
use TypePhp\CompilerBase;
use TypePhp\CompilerTest; use TypePhp\CompilerTest;
final class PropertyCacheCodegenTest extends BaseTest final class PropertyCacheCodegenTest extends BaseTest
{ {
public function testRequestCachesUseOneTlsPointerAndRequestLifetimeStorage(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT);
$compiler->setTargetName('request_cache_storage');
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/property-cache-sites.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$compiler->convertFile($source);
$extension = file_get_contents($compiler->genExtension());
self::assertIsString($extension);
self::assertStringContainsString('#include <new>', $extension);
self::assertStringContainsString('struct php_request_cache_storage final {', $extension);
self::assertStringContainsString('zend_class_entry *class_map[', $extension);
self::assertStringContainsString('zend_function *func_map[', $extension);
self::assertStringContainsString('php::PropertyCacheSlot property_cache_map[', $extension);
self::assertStringContainsString(
'static THREAD_LOCAL php_request_cache_storage *php_request_cache = nullptr;',
$extension,
);
self::assertStringContainsString(
'php_request_cache = new (std::nothrow) php_request_cache_storage{};',
$extension,
);
self::assertStringContainsString('delete php_request_cache;', $extension);
self::assertStringContainsString('php_request_cache = nullptr;', $extension);
self::assertStringNotContainsString('static THREAD_LOCAL zend_class_entry *php_class_map[', $extension);
self::assertStringNotContainsString('static THREAD_LOCAL zend_function *php_func_map[', $extension);
self::assertStringNotContainsString(
'static THREAD_LOCAL php::PropertyCacheSlot php_property_cache_map[',
$extension,
);
self::assertStringNotContainsString('std::memset(php_class_map', $extension);
self::assertStringNotContainsString('std::memset(php_func_map', $extension);
// Module-lifetime caches remain statically allocated and do not enter TLS.
self::assertStringContainsString(
'static php::PersistentCacheSlot<zend_class_entry *> php_persistent_class_map[',
$extension,
);
self::assertStringContainsString(
'static php::PersistentCacheSlot<zend_function *> php_persistent_func_map[',
$extension,
);
self::assertStringContainsString(
'static php::PersistentCacheSlot<uint32_t> php_persistent_property_map[',
$extension,
);
}
public function testOnlyStaticallyNamedPropertySitesReceiveZendCacheSlots(): void public function testOnlyStaticallyNamedPropertySitesReceiveZendCacheSlots(): void
{ {
global $translator; global $translator;

@ -871,6 +871,9 @@ class Translator extends Preprocessor
$this->indentLevel++; $this->indentLevel++;
$code = $this->genIncludeHeaderFiles(); $code = $this->genIncludeHeaderFiles();
// Only the generated module entry allocates request-cache storage.
// Keep <new> out of the shared PCH dependency set used by every source.
$code .= '#include <new>' . PHP_EOL;
if ($this->isBuildModeEmbed()) { if ($this->isBuildModeEmbed()) {
$code .= '#include <typephp_runtime.h>' . PHP_EOL; $code .= '#include <typephp_runtime.h>' . PHP_EOL;
@ -911,16 +914,29 @@ class Translator extends Preprocessor
$code .= 'zend_class_entry *' . $ce . ';' . PHP_EOL; $code .= 'zend_class_entry *' . $ce . ';' . PHP_EOL;
} }
$code .= "// request-local caches \n";
// Keep only one pointer in ELF TLS. Large generated TLS arrays can
// exceed AArch64's static TLS addressing range when Opcache JIT is
// enabled, while the cache entries themselves have request lifetime.
// Ensure each array has at least one element to remain valid C++ when
// a project does not use that cache kind.
$code .= 'struct php_request_cache_storage final {' . PHP_EOL;
$code .= $this->getIndent() . 'zend_class_entry *' . self::CLASS_MAP . '['
. max(1, count($this->classMap)) . ']{};' . PHP_EOL;
$code .= $this->getIndent() . 'zend_function *' . self::FUNC_MAP . '['
. max(1, count($this->funcMap)) . ']{};' . PHP_EOL;
$code .= $this->getIndent() . 'php::PropertyCacheSlot property_cache_map['
. max(1, $this->propertyAccessCacheIndex) . ']{};' . PHP_EOL;
$code .= '};' . PHP_EOL;
$code .= 'static THREAD_LOCAL php_request_cache_storage *php_request_cache = nullptr;' . PHP_EOL;
$code .= "// class entry \n"; $code .= "// class entry \n";
// Ensure the array has at least one element to avoid C/C++ compile errors.
$code .= 'static THREAD_LOCAL zend_class_entry *' . self::PREFIX . self::CLASS_MAP . '[' . max(1, count($this->classMap)) . '];' . PHP_EOL;
// Internal/compiled symbols have module lifetime. They are initialized // Internal/compiled symbols have module lifetime. They are initialized
// lazily after PHP startup, so disable_functions/disable_classes have // lazily after PHP startup, so disable_functions/disable_classes have
// already finalized the runtime tables. ZTS publishes them atomically. // already finalized the runtime tables. ZTS publishes them atomically.
$code .= 'static php::PersistentCacheSlot<zend_class_entry *> ' . self::PREFIX . self::PERSISTENT_CLASS_MAP . '[' . max(1, count($this->persistentClassMap)) . ']{};' . PHP_EOL; $code .= 'static php::PersistentCacheSlot<zend_class_entry *> ' . self::PREFIX . self::PERSISTENT_CLASS_MAP . '[' . max(1, count($this->persistentClassMap)) . ']{};' . PHP_EOL;
$code .= "// func \n"; $code .= "// func \n";
$code .= 'static THREAD_LOCAL zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . max(1, count($this->funcMap)) . '];' . PHP_EOL;
$code .= 'static php::PersistentCacheSlot<zend_function *> ' . self::PREFIX . self::PERSISTENT_FUNC_MAP . '[' . max(1, count($this->persistentFuncMap)) . ']{};' . PHP_EOL; $code .= 'static php::PersistentCacheSlot<zend_function *> ' . self::PREFIX . self::PERSISTENT_FUNC_MAP . '[' . max(1, count($this->persistentFuncMap)) . ']{};' . PHP_EOL;
$code .= $this->genPythonModuleStorage(); $code .= $this->genPythonModuleStorage();
@ -929,38 +945,35 @@ class Translator extends Preprocessor
// No dynamic propMap: the property offset cache only covers declared // No dynamic propMap: the property offset cache only covers declared
// properties of compiled/built-in classes (see getPropertyId). // properties of compiled/built-in classes (see getPropertyId).
$code .= 'static php::PersistentCacheSlot<uint32_t> ' . self::PREFIX . self::PERSISTENT_PROP_MAP . '[' . max(1, count($this->persistentPropMap)) . ']{};' . PHP_EOL; $code .= 'static php::PersistentCacheSlot<uint32_t> ' . self::PREFIX . self::PERSISTENT_PROP_MAP . '[' . max(1, count($this->persistentPropMap)) . ']{};' . PHP_EOL;
// Zend's object handlers use three adjacent pointers as one cache
// entry. Keep these slots request-local: a named access may receive a
// class provided by an ordinary PHP script whose CE is not persistent.
$code .= 'static THREAD_LOCAL php::PropertyCacheSlot ' . self::PREFIX . 'property_cache_map['
. max(1, $this->propertyAccessCacheIndex) . ']{};' . PHP_EOL;
$code .= "// functions \n"; $code .= "// functions \n";
$code .= <<<'CODE' $code .= <<<'CODE'
zend_class_entry *get_class(RequestClassId class_id, const php::Str &class_name) { zend_class_entry *get_class(RequestClassId class_id, const php::Str &class_name) {
const auto index = static_cast<uint32_t>(class_id); const auto index = static_cast<uint32_t>(class_id);
if (UNEXPECTED(php_class_map[index] == nullptr)) { auto &slot = php_request_cache->class_map[index];
php_class_map[index] = php::getClassEntrySafe(class_name); if (UNEXPECTED(slot == nullptr)) {
slot = php::getClassEntrySafe(class_name);
} }
return php_class_map[index]; return slot;
} }
zend_function *get_func(RequestFuncId func_id, const php::Str &func_name) { zend_function *get_func(RequestFuncId func_id, const php::Str &func_name) {
const auto index = static_cast<uint32_t>(func_id); const auto index = static_cast<uint32_t>(func_id);
if (UNEXPECTED(php_func_map[index] == nullptr)) { auto &slot = php_request_cache->func_map[index];
php_func_map[index] = php::getFunction(func_name); if (UNEXPECTED(slot == nullptr)) {
slot = php::getFunction(func_name);
} }
return php_func_map[index]; return slot;
} }
zend_function *get_method(RequestFuncId func_id, const php::Str &method_name, RequestClassId class_id, const php::Str &class_name) { zend_function *get_method(RequestFuncId func_id, const php::Str &method_name, RequestClassId class_id, const php::Str &class_name) {
const auto index = static_cast<uint32_t>(func_id); const auto index = static_cast<uint32_t>(func_id);
if (UNEXPECTED(php_func_map[index] == nullptr)) { auto &slot = php_request_cache->func_map[index];
if (UNEXPECTED(slot == nullptr)) {
auto ce = get_class(class_id, class_name); auto ce = get_class(class_id, class_name);
php_func_map[index] = php::getMethod(ce, method_name); slot = php::getMethod(ce, method_name);
} }
return php_func_map[index]; return slot;
} }
zend_class_entry *get_persistent_class(PersistentClassId class_id, const php::Str &class_name) { zend_class_entry *get_persistent_class(PersistentClassId class_id, const php::Str &class_name) {
@ -994,7 +1007,7 @@ uint32_t get_persistent_prop(PersistentPropertyId prop_id, const php::Str &prop_
} }
php::PropertyCacheSlot &get_property_cache(PropertyCacheId cache_id) { php::PropertyCacheSlot &get_property_cache(PropertyCacheId cache_id) {
return php_property_cache_map[static_cast<uint32_t>(cache_id)]; return php_request_cache->property_cache_map[static_cast<uint32_t>(cache_id)];
} }
CODE; CODE;
$code .= "\n\n"; $code .= "\n\n";
@ -1263,19 +1276,21 @@ CODE;
} }
} }
// User-code symbols have request lifetime regardless of the build mode.
// Embedded/library hosts may start more than one Zend request in the
// same process, so never let these pointers survive RSHUTDOWN.
// Internal/compiled symbols remain in the module-lifetime persistent maps.
$code .= 'std::memset(' . self::PREFIX . self::FUNC_MAP . ', 0, sizeof(' . self::PREFIX . self::FUNC_MAP . '));' . PHP_EOL;
$code .= 'std::memset(' . self::PREFIX . self::CLASS_MAP . ', 0, sizeof(' . self::PREFIX . self::CLASS_MAP . '));' . PHP_EOL;
$code .= '}' . PHP_EOL . PHP_EOL; $code .= '}' . PHP_EOL . PHP_EOL;
// module_clean end // module_clean end
$moduleName = $this->getModuleName(); $moduleName = $this->getModuleName();
// rinit begin // rinit begin
$code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL; $code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL;
$code .= 'if (UNEXPECTED(php_request_cache != nullptr)) {' . PHP_EOL;
$code .= $this->getIndent() . 'php_error_docref(nullptr, E_WARNING, "TypePHP request cache is already initialized");' . PHP_EOL;
$code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL;
$code .= '}' . PHP_EOL;
$code .= 'php_request_cache = new (std::nothrow) php_request_cache_storage{};' . PHP_EOL;
$code .= 'if (UNEXPECTED(php_request_cache == nullptr)) {' . PHP_EOL;
$code .= $this->getIndent() . 'php_error_docref(nullptr, E_WARNING, "Unable to allocate TypePHP request cache");' . PHP_EOL;
$code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL;
$code .= '}' . PHP_EOL;
$code .= 'php::request_init();' . PHP_EOL; $code .= 'php::request_init();' . PHP_EOL;
$code .= 'module_init();' . PHP_EOL; $code .= 'module_init();' . PHP_EOL;
@ -1311,9 +1326,8 @@ CODE;
$code .= <<<CODE $code .= <<<CODE
PHP_RSHUTDOWN_FUNCTION({$moduleName}) { PHP_RSHUTDOWN_FUNCTION({$moduleName}) {
for (auto &slot : php_property_cache_map) { delete php_request_cache;
slot.reset(); php_request_cache = nullptr;
}
php::request_shutdown(); php::request_shutdown();
module_clean(); module_clean();
return SUCCESS; return SUCCESS;

Loading…
Cancel
Save