perf(parser): optimize array dimension fetch and selection operations

- Cache GLOBALS variable check result to avoid redundant parsing
- Implement fast paths for typed array and string reads using direct method calls
- Add optimized handling for simple shorthand ternary operations
- Generate efficient C++ code for array dimension fetch with proper type detection
- Preserve PHP semantics for reference handling in array operations
- Add comprehensive test coverage for typed read optimizations
- Improve namespace isolation for generated runtime symbols across build modes
master
韩天峰 4 days ago
parent 4713a58b06
commit d80f4a0d20
  1. 12
      phpunit/code/hot-path-codegen.php
  2. 48
      phpunit/src/CompilerBaseApiTest.php
  3. 25
      phpunit/src/HotPathCodegenTest.php
  4. 29
      src/Parser/ArrayExpressionTrait.php
  5. 4
      src/Parser/AssignOpTrait.php
  6. 39
      src/Parser/SelectionExpressionTrait.php
  7. 5
      src/Parser/TypeConversionTrait.php
  8. 64
      tests/compiler/optimizations/typed-read-fast-path.phpt

@ -32,3 +32,15 @@ function hotPathCodegen(int $limit): int
return $assigned + $length + strlen($safe) + strlen($ordered) + $value;
}
function hotPathTypedReads(array $hash, string $str, bool $flag, int $fallback): void
{
$hashValue = $hash['value'];
$stringValue = $str[0];
$arraySelection = $hash ?: null;
$boolSelection = $flag ?: $fallback;
// A compound left operand still needs the generic chain helper so the
// array lookup is evaluated exactly once.
$chainedSelection = $hash['value'] ?: $fallback;
}

@ -970,6 +970,54 @@ YAML);
}
}
public function testGeneratedRuntimeSymbolsUseProjectNamespace(): void
{
global $translator;
$testFile = ROOT_PATH . '/phpunit/code/compiler_api/extension_clean_maps.php';
foreach ([
CompilerBase::BUILD_MODE_EXT => 'isolated_ext',
CompilerBase::BUILD_MODE_BIN => 'isolated_bin',
CompilerBase::BUILD_MODE_LIB => 'isolated_lib',
] as $mode => $target) {
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$compiler->setBuildMode($mode);
$compiler->setTargetName($target);
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
$dataFile = $this->testDir . '/' . $target . '_data_decl.h';
$compiler->genDataDeclarations($dataFile);
$data = file_get_contents($dataFile);
$extension = file_get_contents($compiler->genExtension());
$namespace = 'typephp_' . $target;
$this->assertStringContainsString('namespace ' . $namespace . ' {', $data, $mode);
$this->assertStringContainsString('using namespace ' . $namespace . ';', $data, $mode);
$this->assertStringContainsString('zend_class_entry *php_get_class(', $data, $mode);
$this->assertStringContainsString('namespace ' . $namespace . ' {', $extension, $mode);
$this->assertStringContainsString('zend_class_entry *php_get_class(', $extension, $mode);
if ($mode === CompilerBase::BUILD_MODE_BIN) {
$this->assertStringContainsString('zend_module_entry *php_embed_get_module()', $extension);
$this->assertStringContainsString(
'return &' . $namespace . '::' . $namespace . '_module_entry;',
$extension,
);
} elseif ($mode === CompilerBase::BUILD_MODE_LIB) {
$this->assertStringNotContainsString('zend_module_entry *php_embed_get_module()', $extension);
$this->assertStringContainsString(
'zend_module_entry *php_' . $target . '_embed_get_module()',
$extension,
);
} else {
$this->assertStringNotContainsString('php_embed_get_module', $extension, $mode);
}
}
}
public function testFunctionPointerCacheRejectsClassMethodNames(): void
{
$this->expectException(\LogicException::class);

@ -11,8 +11,8 @@ final class HotPathCodegenTest extends \BaseTest
self::assertStringContainsString('items.item(0L, true) = value;', $code);
self::assertStringContainsString('items.append(value);', $code);
self::assertStringContainsString('items.item(0L, true) += value;', $code);
self::assertStringContainsString('items.item(0L, true) += other.item(0L, false);', $code);
self::assertStringContainsString('items.item(2L, true) = other.item(0L, false);', $code);
self::assertStringContainsString('items.item(0L, true) += other.get(0L);', $code);
self::assertStringContainsString('items.item(2L, true) = other.get(0L);', $code);
self::assertStringContainsString('items.offsetSet(0L,', $code);
}
@ -36,6 +36,27 @@ final class HotPathCodegenTest extends \BaseTest
self::assertDoesNotMatchRegularExpression('/php::Var (tmp_var_\d+);[\s\S]*?\\1 = limit--;/', $code);
}
public function testTypedReadsAndSimpleShorthandTernariesUseFastPaths(): void
{
$code = $this->compileFixture();
self::assertMatchesRegularExpression(
'/hash\.get\(_literal_strings\[\d+\]\)/',
$code,
);
self::assertStringContainsString('str.offsetGet(0L)', $code);
self::assertStringNotContainsString('php::notEmpty(hash, {},', $code);
self::assertStringNotContainsString('php::notEmpty(flag, {},', $code);
self::assertStringContainsString('php::toBool(hash)', $code);
self::assertStringContainsString('php::toBool(flag)', $code);
// Compound receivers retain the evaluate-once chain implementation.
self::assertMatchesRegularExpression(
'/php::notEmpty\(hash, \{\{php::ArrayDimFetch, php::Var\(_literal_strings\[\d+\]\)\}\}, tmp_var_\d+\)/',
$code,
);
}
private function compileFixture(): string
{
global $translator;

@ -203,9 +203,10 @@ trait ArrayExpressionTrait
return $this->parseStdContainerDimFetch($node);
}
$isGlobals = $this->isVarExpr($node->var) && $node->var->name === 'GLOBALS';
$var = $write ? $this->parseWritableIdentifier($node->var) : $this->parseIdentifier($node->var);
if ($this->isVarExpr($node->var)) {
if ($var === 'GLOBALS') {
if ($isGlobals) {
return $this->parseGlobalsArrayDimFetch($node);
}
if (!$this->hasVar($var)) {
@ -235,6 +236,32 @@ trait ArrayExpressionTrait
}
} else {
$dim = $this->parseIdentifier($node->dim);
// Only fixed local variables and parameters are emitted as
// php::Array or php::Str. Globals/statics, properties, and call
// expressions may carry the same PHP type while their C++ storage
// remains php::Variant.
$fixedReceiver = $this->isVarExpr($node->var)
&& !$this->hasScopeGlobalVar($var)
&& !$this->hasStaticVar($var);
if (!$write && $fixedReceiver) {
$receiverType = $this->detectTypeOfExpr($node->var);
$dimType = $this->detectTypeOfExpr($node->dim);
if ($receiverType === Type::STR) {
$offset = $dimType === Type::INT ? $dim : 'php::toInt(' . $dim . ')';
return $var . '.offsetGet(' . $offset . ')';
}
if ($receiverType === Type::ARRAY) {
if ($dimType === Type::INT) {
return $var . '.get(' . $dim . ')';
}
if ($dimType === Type::FLOAT) {
return $var . '.get(php::toInt(' . $dim . '))';
}
if ($dimType === Type::STR) {
return $var . '.get(' . $dim . ')';
}
}
}
return $var . '.item(' . $dim . ', ' . $this->escapeBool($write) . ')';
}
}

@ -911,6 +911,10 @@ trait AssignOpTrait
return $this->parseStdContainerAssignOp($node, $op);
}
if ($this->canUpdateKnownArraySlotInPlace($node, $op)) {
if ($this->isVarExpr($node->var->var) && $node->var->var->name === 'GLOBALS') {
$slot = $this->parseGlobalsArrayDimFetch($node->var);
return $slot . ' ' . $op . ' ' . $this->parseExprAsValue($node->expr);
}
$array = $this->parseWritableIdentifier($node->var->var);
$dim = $this->parseIdentifier($node->var->dim);
return $array . '.item(' . $dim . ', true) ' . $op . ' '

@ -284,15 +284,25 @@ trait SelectionExpressionTrait
$this->checkVarMustExist($left, $leftExpr);
}
$condExpr = $this->parseChainedExpr($left, $op, true);
$chainOpResult = $left->getAttribute('chainOpResult');
if ($chainOpResult) {
$leftExpr = $chainOpResult;
}
$leftType = $this->detectTypeOfExpr($left);
if ($op === self::OP_NOT_EMPTY
&& in_array($leftType, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) {
$condExpr = '((' . $condExpr . '), ' . $this->convertBoolExpr($leftExpr, $leftType) . ')';
$simpleShorthand = $op === self::OP_NOT_EMPTY
&& $this->isVarExpr($left)
&& $leftType !== Type::REF;
if ($simpleShorthand) {
// A local variable is already a stable value. Avoid routing it
// through the generic operation-chain walker, which otherwise
// copies the value into a Variant result before testing it.
$condExpr = $this->convertConditionExpr($left, $leftExpr);
} else {
$condExpr = $this->parseChainedExpr($left, $op, true);
$chainOpResult = $left->getAttribute('chainOpResult');
if ($chainOpResult) {
$leftExpr = $chainOpResult;
}
if ($op === self::OP_NOT_EMPTY
&& in_array($leftType, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) {
$condExpr = '((' . $condExpr . '), ' . $this->convertBoolExpr($leftExpr, $leftType) . ')';
}
}
$rightBeforeStmtCount = count($this->context->beforeStmtLines);
@ -304,6 +314,19 @@ trait SelectionExpressionTrait
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $rightAfterStmtCount);
$this->checkVarMustExist($right, $rightExpr);
if ($simpleShorthand && !$rightBeforeStmts && !$rightAfterStmts) {
// C++'s conditional operator evaluates the condition first and
// only the selected branch. Explicit Variant materialization is
// needed when the PHP branch types differ; otherwise C++ could
// choose a common scalar type (for example bool -> int).
$rightType = $this->detectTypeOfExpr($right);
if ($leftType !== $rightType) {
$leftExpr = 'php::Var(' . $leftExpr . ')';
$rightExpr = 'php::Var(' . $rightExpr . ')';
}
return '(' . $condExpr . ') ? (' . $leftExpr . ') : (' . $rightExpr . ')';
}
$tmpVar = $this->addTmpVar(Type::VAR);
$comment = $this->debug
? $this->formatCppLineComment('Expr: ', $this->printer->prettyPrintExpr($expr)) . PHP_EOL

@ -266,11 +266,14 @@ trait TypeConversionTrait
{
$this->assertNativeObjectReferenceForbidden($expr, $expr);
$this->checkLeftValue($expr);
if ($expr instanceof Node\Expr\ArrayDimFetch) {
return $this->parseArrayDimFetchUpdate($expr) . '.toReference()';
}
$var = $this->parseIdentifier($expr);
if ($this->isVarExpr($expr) and $this->isNativeTypeVar($var)) {
$this->context->localVars[$var] = Type::VAR;
}
return $this->parseIdentifier($expr) . '.toReference()';
return $var . '.toReference()';
}
}

@ -0,0 +1,64 @@
--TEST--
typed array/string reads and shorthand ternaries preserve PHP semantics
--FILE--
<?php
function inspectTypedReads(array $values, string $key, string $text, int $offset): void
{
var_dump($values['value']);
var_dump($values[$key]);
var_dump($text[0]);
var_dump($text[$offset]);
var_dump($text[-1]);
}
function inspectSelections(array $values, string $text, bool $flag): void
{
var_dump($values ?: ['fallback']);
var_dump($text ?: 'fallback');
var_dump($flag ?: 42);
}
function main(): void
{
$referenced = 3;
$values = [
'value' => 1,
'12' => 'numeric key',
'ref' => &$referenced,
];
inspectTypedReads($values, '12', 'test', 1);
$copy = $values['ref'];
$copy = 4;
var_dump($referenced, $copy);
inspectSelections($values, '0', false);
inspectSelections([], 'ok', true);
}
?>
--EXPECT--
int(1)
string(11) "numeric key"
string(1) "t"
string(1) "e"
string(1) "t"
int(3)
int(4)
array(3) {
["value"]=>
int(1)
[12]=>
string(11) "numeric key"
["ref"]=>
&int(3)
}
string(8) "fallback"
int(42)
array(1) {
[0]=>
string(8) "fallback"
}
string(2) "ok"
bool(true)
Loading…
Cancel
Save