TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

24 KiB

In-Place Optimization Plan for High-Precision Types

1. Background

TypePHP currently implements BigInt, BigFloat, and Decimal as PHPX Box objects stored in Zend resources. High-precision operations use an immutable result interface, for example:

target = php::BigInt::mul(target, rhs);

Even when the PHP source uses compound assignment:

$target *= $rhs;

The compiler still generates code that "creates a new result and reassigns". Taking BigInt as an example, a single multiplication currently typically requires:

  1. Create a new BigInt Box.
  2. Register a new Zend resource.
  3. Initialize a new mpz_t.
  4. Allocate GMP limb storage for the computed result.
  5. Move-assign the new resource to the target variable.
  6. Destruct the old resource, Box, and underlying numeric storage.

In scenarios such as loops, accumulation, factorials, and monetary aggregation, these overheads grow linearly with the number of operations:

for ($i = 0; $i < $count; $i++) {
    $value = $value * 1000;
}

The underlying objects of GMP, MPFR, and mpdecimal are not immutable; all three support in-place operations where the output overlaps the input. The current immutable behavior comes from PHPX's high-precision API, not from a limitation of the underlying math libraries.

This document provides the implementation plan, semantic constraints, phased plan, and acceptance criteria for in-place operations on high-precision types.

2. Optimization Goals

2.1 Primary Goals

  • Reuse existing objects for uniquely-held BigInt, BigFloat, and Decimal Boxes.
  • Reuse GMP limb, MPFR mantissa, and mpdecimal coefficient storage as much as possible.
  • Eliminate the result Box and Zend resource temporary objects in compound assignments.
  • Use php::Int or php::Var directly for native RHS values such as integers, avoiding the construction of high-precision RHS Boxes.
  • Fuse the safe $x = $x {op} $rhs pattern into an in-place operation.
  • Preserve PHP's value semantics, reference semantics, evaluation order, and exception behavior.
  • Automatically fall back to the current immutable implementation in unsafe or unprovably-safe scenarios.

2.2 Non-Goals

  • The first phase does not optimize complex lvalues such as array elements, dynamic properties, and property hooks.
  • It does not rely on whole-program alias analysis to guarantee correctness.
  • It does not modify GMP, MPFR, or mpdecimal third-party source code.
  • It does not turn all ordinary binary expressions into mutable computations.
  • It does not change the implicit conversion rules between different high-precision types.

3. Underlying Library Capabilities

Type Underlying object In-place operation Memory reuse characteristics
BigInt GMP mpz_t / mpz_class Supported Reuses limb when capacity is sufficient, grows only when the result grows
BigFloat MPFR mpfr_t Supported Currently fixed 256-bit precision, ordinary operations can usually keep reusing the mantissa
Decimal mpdecimal mpd_t / decimal::Decimal Supported Can reuse the coefficient; the library itself provides operator+= and other in-place interfaces

Typical in-place calls are as follows:

mpz_mul(dst, dst, rhs);
mpfr_mul(dst, dst, rhs, MPFR_RNDN);
mpd_qmul(dst, dst, rhs, context, &status);

mpdecimal's C++ wrapper already provides:

Decimal::operator+=
Decimal::operator-=
Decimal::operator*=
Decimal::operator/=
Decimal::operator%=

Therefore the technical bottleneck lies mainly in PHP Box sharing semantics, exception safety, and the compiler's evaluation order, rather than in the math libraries themselves.

4. Language Semantics That Must Be Preserved

4.1 Copy-on-write

A high-precision value may be shared by multiple PHP variables through the same Box:

$a = std::bigInt(10);
$b = $a;
$a *= 2;

The result must be:

$a = 20
$b = 10

The shared Box must not be modified directly. PHPX must check the Zend resource reference count before operating:

  • Resource uniquely held: modify the original Box directly.
  • Resource shared: copy the Box, bind the target variable to the copy, then modify the copy.

Runtime copy-on-write is the last line of defense for correctness. Compiler static analysis only reduces unnecessary checks and identifies fusable expressions; it cannot replace the runtime check.

4.2 PHP References

In the following scenario, two variables point to the same PHP reference container:

$a = std::bigInt(10);
$b =& $a;
$a *= 2;

The result must be that both $a and $b become 20. The in-place API must operate on the actual zval inside the reference through Variant::unwrap_ptr(); when copy-on-write occurs, it should update the value in the reference container rather than rebinding the PHPX wrapper object.

4.3 RHS and Target Variable Aliasing

The following must be handled correctly:

$a *= $a;

It is recommended that the in-place interface take the target by reference and the RHS by value:

BigInt::mulAssign(Variant &target, Variant rhs);

If the RHS shares the same resource as the target, the RHS's temporary reference count will cause copy-on-write to take the copy branch. This may miss an in-place opportunity, but it naturally guarantees correctness. A dedicated path for "RHS and target are the same Box" can be added later.

4.4 Evaluation Order

The following two pieces of code cannot be treated as equivalent in all cases:

$x = $x * changeValue($x);
$x *= changeValue($x);

The RHS may reassign, modify by reference, or modify $x through closure capture. C++ function argument evaluation order cannot be used to replace PHP's evaluation rules either.

The compiler must follow these rules:

  • Use the existing ordered-operand and side-effect capture mechanism for true AssignOp.
  • For $x = $x {op} $rhs, fuse only when the RHS does not write to or escape $x.
  • Use the current "compute new result then assign" path when safety cannot be proven.
  • If the old $x must be saved to preserve ordering, that temporary increases the reference count, and runtime copy-on-write fallback should be allowed automatically.

4.5 Complex Lvalues

The following expressions must not be rewritten in the first phase:

$array[getIndex()] = $array[getIndex()] * 2;
$object->value = $object->value * 2;
$object->hooked = $object->hooked * 2;

Reasons include:

  • The subscript expression may execute twice.
  • The number of calls to getters, setters, or property hooks may change.
  • Dynamic property reads/writes may trigger magic methods.
  • The lvalue itself may have side effects.

The first phase only supports simple local variables. Complex lvalues are designed separately in later phases through a "single-evaluation writable target" abstraction.

4.6 Exception Safety

The current immutable implementation computes the new result first and only assigns after success, so the target variable remains unchanged when an exception occurs:

$value = std::decimal('10');

try {
    $value /= 0;
} catch (DivisionByZeroError $e) {
}

echo $value; // still 10

The in-place implementation must preserve this behavior.

  • BigInt: Check error conditions such as the divisor, modulus, and exponent before modifying.
  • BigFloat: Check division-by-zero and error conditions explicitly defined by the current API before modifying.
  • Decimal: context.raise(status) may throw after the underlying result has already been written, requiring a transactional commit or rollback mechanism.
  • Memory allocation failure must also not leave the target in a partially-modified state.

4.7 Resource identity

High-precision Boxes are currently exposed as resources, and get_resource_id() and strict comparison may observe resource identity. In-place operations keep the resource id for uniquely-held variables, whereas the current immutable implementation generates a new resource id.

One of the following contracts must be clarified before implementation:

  1. High-precision types are value types; resource identity is an internal implementation detail and is not guaranteed to remain unchanged across operations.
  2. The current resource identity change must be preserved, in which case only the underlying numeric storage can be reused and the resource must be rewrapped, reducing the benefit.

Option 1 is recommended, and the high-precision type documentation should make it explicit: users should compare values and should not rely on internal resource ids. The value semantics of shared variables are still strictly guaranteed by copy-on-write.

5. PHPX Design

5.1 Explicit In-Place API

It is not recommended to add high-precision operator overloading to the generic Variant. Explicit interfaces should be added to each high-precision type:

class BigInt {
  public:
    static Variant &addAssign(Variant &target, Variant rhs);
    static Variant &subAssign(Variant &target, Variant rhs);
    static Variant &mulAssign(Variant &target, Variant rhs);
    static Variant &divAssign(Variant &target, Variant rhs);
    static Variant &modAssign(Variant &target, Variant rhs);
};

BigFloat and Decimal use the same naming convention. BigInt should also cover bitwise operations and shifts:

bitAndAssign
bitOrAssign
bitXorAssign
bitShiftLeftAssign
bitShiftRightAssign

The interface returns Variant &, so that compound assignment can still be used as an expression:

$result = ($value *= 2);

If the actual generated code is inconvenient to handle the reference return, a statement-only void fast path can be provided at the same time, but the assignment expression semantics must not be sacrificed.

5.2 Box Uniqueness Utility

Provide a reusable C++17 helper inside PHPX instead of duplicating Zend resource logic across the three types:

template <typename T>
T *separateBoxForWrite(Variant &target);

Responsibilities include:

  1. Dereference indirect/reference zvals.
  2. Verify that target is the target Box type.
  3. Check the Zend resource reference count.
  4. Return the original Box when uniquely held.
  5. Copy the Box when shared, and update the target through Variant assignment semantics.
  6. Preserve typed reference checks and exception propagation.

All three Boxes must support correct copying:

  • BigInt: copy the mpz_class.
  • BigFloat: initialize at the source precision and copy the mpfr_t.
  • Decimal: copy the decimal::Decimal.

5.3 RHS Extraction

The in-place interface should accept Variant rhs directly and reuse the existing operand extractor:

  • php::Int is converted directly to an underlying integer operand.
  • php::Var checks its actual type at runtime.
  • When already a Box of the same type, read the underlying value directly.
  • Strings, floats, and different high-precision types continue to follow the current conversion restrictions.

The generated code should prioritize:

php::BigInt::mulAssign(value, 1000L);
php::Decimal::mulAssign(value, factor);

Avoid:

php::BigInt::mulAssign(value, php::toBigInt(1000L));
php::Decimal::mulAssign(value, php::toDecimal(1000L));

For Decimal's integer RHS, mpdecimal's _i64/_u64 interfaces can be used further to avoid constructing a temporary decimal::Decimal:

mpd_qmul_i64(result, left, rhs, context, &status);

5.4 BigInt Implementation Strategy

BigInt prioritizes true in-place operations:

Variant &BigInt::mulAssign(Variant &target, Variant rhs) {
    BigIntOperand right;
    // Extract and validate the RHS first.
    // Then perform copy-on-write on target.
    // Finally call mpz_mul(dst, dst, right).
    return target;
}

All recoverable error checks, such as division by zero, modulo by zero, and illegal shift amounts, must be completed before modifying. GMP capacity growth is managed internally; the original limb storage is reused when capacity is sufficient.

5.5 BigFloat Implementation Strategy

BigFloat currently uniformly uses BIG_FLOAT_DEFAULT_PRECISION, which is suitable for direct in-place operations:

mpfr_mul(dst, dst, rhs, MPFR_RNDN);

If per-object precision is supported in the future, the relationship between the non-in-place result precision and the compound-assignment target precision must be specified, and tests for objects of different precisions must be added.

5.6 Decimal Implementation Strategy

Decimal is implemented in two steps.

The first step uses exception-safe transactional commit:

decimal::Decimal temporary;
uint32_t status = 0;
mpd_qmul(temporary.get(), current.getconst(), rhs, context, &status);
context.raise(status);
current = std::move(temporary);

This approach can eliminate the result Box and Zend resource, but still creates an underlying Decimal temporary object.

The second step evaluates true in-place operations:

  • Complete explicit checks such as division-by-zero before modifying.
  • Identify which status/trap values may throw after the operation.
  • Provide backup/rollback for operations that may throw, or only perform in-place when it can be proven that no trap will be triggered.
  • Run dedicated tests for Overflow, InvalidOperation, DivisionByZero, and simulated allocation failure.

"Target value partially modified after an exception" must not be accepted for the sake of performance.

6. Compiler Design

6.1 True Compound Assignment

First modify the existing Big* AssignOp generation path:

$value *= $rhs;

From:

value = php::BigInt::mul(value, rhs);

To:

php::BigInt::mulAssign(value, rhs);

Support matrix:

Type First-phase operators
BigInt `+= -= *= /= %= &=
BigFloat += -= *= /=
Decimal += -= *= /= %=

6.2 Ordinary Assignment Fusion

Identify the following AST:

$x = $x {op} $rhs;

Fuse only when all of the following conditions are met:

  • The lvalue is a simple named variable.
  • The left operand of the binary expression is the same variable.
  • The variable's static type is BigInt, BigFloat, or Decimal.
  • The operator is in the corresponding type's supported list.
  • The RHS does not contain an assignment to, a reference acquisition of, or a known by-reference argument passing of the target variable.
  • The RHS does not contain eval, dynamic calls, or other escape paths that cannot be safely analyzed; or the existing side-effect analysis clearly proves safety.
  • The current expression context can correctly receive the in-place interface's return value.

The following scenarios are not fused in the first phase:

$x = 2 - $x;
$x = $x * ($x = 2);
$x = $x * dynamicCall();
$array[$key] = $array[$key] * 2;
$object->value = $object->value * 2;

Optimization of commutative operations such as $x = $rhs + $x or $x = $rhs * $x is deferred to later phases to avoid expanding the scope of the first version.

6.3 Failure Fallback

The optimization must be an optional codegen path:

Can safely operate in-place -> emit *Assign()
Cannot prove safety        -> emit the current new-result path

Any type uncertainty, complex lvalue, reference escape, or side-effect analysis failure must not cause a compilation error; it should only lose that optimization.

6.4 Relationship with SSA/Optimizer

The initial version can perform local AST matching in AssignOpTrait and ordinary assignment resolution without relying on a complete SSA.

Later, SSA can provide:

  • Whether the target variable has aliases.
  • Whether the RHS writes to the target variable.
  • Whether the variable escapes to dynamic calls or references.
  • Whether it can statically prove the Box is uniquely held.

Even if SSA proves uniqueness, the PHPX runtime copy-on-write check is still recommended to be retained, unless there is a strict escape proof and dedicated tests.

7. Phased Implementation Plan

Phase 0: Baseline and Observation

  • Add test helper facilities for counting high-precision Box/resource creation.
  • Establish benchmarks for BigInt, BigFloat, and Decimal loop operations.
  • Record current wall time, Box count, resource count, and underlying allocation count.
  • Freeze the current aliasing, reference, exception, and resource identity behavior.

Deliverable: a baseline report and behavior tests, with no change to generated code.

Phase 1: Native RHS Fast Path

  • BigInt operations directly accept php::Int.
  • BigFloat operations directly accept php::Int, php::Float.
  • Decimal operations directly accept php::Int and php::Var that is actually an int.
  • The Decimal integer path prioritizes mpd_q*_i64.
  • Eliminate the high-precision Box the compiler creates for the RHS.

Deliverable: no more unnecessary toBigInt(), toBigFloat(), toDecimal() on the RHS.

Phase 2: PHPX Copy-on-write Infrastructure

  • Implement separateBoxForWrite<T>().
  • Complete copy tests for the three Box types.
  • Cover ordinary variables, shared variables, PHP references, indirect zvals, and RHS being the same Box.
  • Clarify the resource identity contract.

Deliverable: standalone PHPX unit tests, with no modification to the compiler generation path.

Phase 3: BigInt and BigFloat Compound Assignment

  • Implement the BigInt *Assign() method family.
  • Implement the BigFloat *Assign() method family.
  • Modify the generated code for true PHP AssignOp.
  • Preserve fallback for unsafe paths.
  • Run the full PHPX test suite, full compiler PHPUnit, relevant PHPT, and bootstrap compilation.

Deliverable: syntax such as $x *= $rhs uses true in-place operations.

Phase 4: Ordinary Assignment Fusion

  • Identify simple local variables $x = $x {op} $rhs.
  • Implement target variable write/escape checks.
  • Prioritize enabling for pure-literal and pure-variable RHS.
  • Preserve the old path for RHS with side effects.

Deliverable: common patterns in the problem description no longer require users to manually convert to compound assignment.

Phase 5: Decimal Transactional In-Place Interface

  • Implement the Decimal *Assign() API.
  • First use "underlying temporary result + commit on success".
  • Optimize integer RHS using the _i64 fast path.
  • Cover all Decimal traps and the target value after exceptions.

Deliverable: eliminate the Decimal result Box/resource while maintaining strong exception safety.

Phase 6: Decimal True In-Place Computation

  • Analyze the status/trap values each operator may trigger.
  • Directly use the target mpd_t for operations that can be proven safe.
  • Preserve the transactional path for high-risk operations.
  • Determine through benchmarks whether the complexity is worthwhile.

Deliverable: common Decimal accumulation operations reuse coefficient storage.

Phase 7: Complex Lvalues and Further Optimizations

  • Design a single-evaluation writable target abstraction.
  • Evaluate support for array elements, static properties, and ordinary properties.
  • Property hooks, magic methods, and dynamic properties are not enabled by default unless the number of calls and ordering can be strictly preserved.
  • Evaluate commutative expression fusion and SSA uniqueness proof.

8. Test Plan

8.1 PHPX Unit Tests

Each type and each operator must at least cover:

  • Unique Box in-place update.
  • Shared Box triggers copy-on-write.
  • PHP references update the same referenced value.
  • RHS and target are the same Box.
  • Allowed RHS types such as Int, Float, String, and Var.
  • Exceptions for illegal RHS types.
  • Edge cases such as division by zero, modulo by zero, and negative exponents.
  • The target value remains unchanged after an exception.
  • Capacity growth triggered by extremely large numbers.
  • Multiple consecutive operations.

8.2 Compiler PHPUnit

Check the generated code:

  • AssignOp generates calls such as BigInt::mulAssign().
  • $x = $x * 1000 is fused.
  • RHS native integers no longer construct Big* Boxes.
  • No fusion when the RHS has side effects.
  • Array elements and properties are not fused in the first phase.
  • Unsupported operators continue to produce the original FatalError.

8.3 PHPT

At least cover:

$a *= 2;
$a = $a * 2;
$b = $a; $a *= 2;
$b =& $a; $a *= 2;
$a *= $a;
$a *= ($factor = 2);
$result = ($a *= 2);

And cover for the three high-precision types:

  • Positive, negative, and zero values.
  • Extreme values and precision boundaries.
  • All supported compound assignment operators.
  • The lvalue after an exception.
  • Consecutive updates in a loop.

8.4 Integration Verification

Each phase must at least execute:

./vendor/bin/phpunit
php run-tests.php tests/compiler/bigint tests/compiler/bignumber tests/compiler/decimal
php bin/tpc.php project.yml

PHPX modifications must also run the full PHPX unit test suite.

9. Performance Acceptance

Performance tests must at least include:

  • Sizes of 1, 4, 16, 64, 256, and 1024 limb/decimal digits.
  • RHS being small integers, same-type high-precision values, and dynamic php::Var.
  • Unique Box and shared Box.
  • Loops of 1 thousand, 100 thousand, and 1 million iterations.
  • BigInt growth multiplication versus stable-capacity addition.
  • BigFloat fixed-precision accumulation.
  • Decimal fixed 50-digit precision accumulation.

Functional acceptance criteria:

  • Compound assignment of a unique BigInt/BigFloat does not create a result Box/resource per iteration.
  • Native RHS does not create a high-precision Box.
  • Shared Box correctly triggers copy-on-write.
  • All exception paths keep the target value unchanged.
  • Bootstrap compilation and full test suites pass.

Performance acceptance is based on baseline data and does not preset unrealistic fixed multiples. At least the following should be reported separately:

  • Total elapsed time.
  • Box/resource creation counts.
  • Underlying memory allocation counts and bytes.
  • Peak memory.
  • Copy-on-write hit rate and fallback rate.

If an optimization path cannot reduce allocations, or causes clear regression in common non-in-place expressions, the old path should be retained or that sub-optimization should be reverted.

10. Risks and Rollback Strategy

Main risks:

  • Incorrect Box sharing determination causing other variables to be modified unexpectedly.
  • References or indirect zvals being rebound instead of updated.
  • RHS side effects changing the evaluation order.
  • Decimal target value being polluted after an exception.
  • Undocumented changes in resource identity behavior.
  • In-place capacity growth failure leaving an invalid underlying object.

Control measures:

  • All optimizations are concentrated in a standalone PHPX API and a single compiler codegen branch.
  • Fall back to the old implementation when safety cannot be proven.
  • Enable incrementally by type and by operator.
  • Commit each phase independently, avoiding modifying too many semantics at once.
  • Do not remove the existing immutable API until exception, aliasing, and reference tests are complete.

Rollback only requires the compiler to regenerate:

target = Type::operation(target, rhs);

The original immutable API must be retained throughout the entire migration period.

Considering benefit, complexity, and risk, the recommended order is:

  1. BigFloat in-place compound assignment.
  2. BigInt in-place compound assignment.
  3. BigInt/BigFloat ordinary assignment fusion.
  4. Decimal native integer RHS fast path.
  5. Decimal transactional *Assign().
  6. Decimal true in-place computation.
  7. Complex lvalues and SSA enhancements.

BigFloat has fixed precision and is the easiest to stably reuse underlying memory; BigInt has broader applications and its overall benefit may be the largest; Decimal has the most complex exception and trap semantics, and its true in-place modification should be pushed last.

12. Final Target Code

For safe simple variables:

$value = $value * 1000;

The final generation:

php::BigInt::mulAssign(value, 1000L);

Runtime:

Unique Box: reuse Box, resource, and underlying storage in place
Shared Box: copy-on-write, then modify the new Box
Unsafe scenario: fall back to the current immutable result implementation

This design confines the performance optimization within verifiable boundaries while preserving the consistency of TypePHP with PHP assignment, reference, and exception semantics.