Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement acyclic object tracking #17130

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions Zend/tests/gc/gc_045.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class GlobalData

class Value
{
/* Force object to be added to GC, even though it is acyclic. */
public $dummy;

public function __destruct()
{
new Bar();
Expand All @@ -19,6 +22,9 @@ class Value

class Bar
{
/* Force object to be added to GC, even though it is acyclic. */
public $dummy;

public function __construct()
{
GlobalData::$bar = $this;
Expand Down
3 changes: 3 additions & 0 deletions Zend/tests/weakrefs/gh10043-008.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Self-referencing map entry GC - 008

class Canary extends stdClass
{
/* Force object to be added to GC, even though it is acyclic. */
public $dummy;

public function __construct(public string $name)
{
}
Expand Down
42 changes: 42 additions & 0 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -2390,6 +2390,8 @@ ZEND_API zend_result zend_startup_module_ex(zend_module_entry *module) /* {{{ */
}
module->module_started = 1;

uint32_t prev_class_count = zend_hash_num_elements(CG(class_table));

/* Check module dependencies */
if (module->deps) {
const zend_module_dep *dep = module->deps;
Expand Down Expand Up @@ -2434,6 +2436,19 @@ ZEND_API zend_result zend_startup_module_ex(zend_module_entry *module) /* {{{ */
}
EG(current_module) = NULL;
}

/* Mark classes with custom get_gc handler as potentially cyclic, even if
* their properties don't indicate so. */
Comment on lines +2442 to +2443
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also mark classes with custom get_properties handler as potentially cyclic, as this is called by zend_std_get_gc.

Otherwise this looks right to me, as objects with std get_gc and get_properties can not expose anything other than standard properties to the GC.

Lazy objects need to take care of updating GC_NOT_COLLECTABLE: Remove GC_NOT_COLLECTABLE in zend_object_make_lazy() when the initializer is cyclic or may have a ref to the object itself, and add it again in zend_lazy_object_init().

if (prev_class_count != zend_hash_num_elements(CG(class_table))) {
Bucket *p;
ZEND_HASH_MAP_FOREACH_BUCKET_FROM(CG(class_table), p, prev_class_count) {
zend_class_entry *ce = Z_PTR(p->val);
if (ce->default_object_handlers->get_gc != zend_std_get_gc) {
ce->ce_flags |= ZEND_ACC_MAY_BE_CYCLIC;
}
} ZEND_HASH_FOREACH_END();
}

return SUCCESS;
}
/* }}} */
Expand Down Expand Up @@ -4494,6 +4509,27 @@ static zend_always_inline bool is_persistent_class(zend_class_entry *ce) {
&& ce->info.internal.module->type == MODULE_PERSISTENT;
}

static bool zend_type_may_be_cyclic(zend_type type)
{
if (!ZEND_TYPE_IS_SET(type)) {
return true;
}

if (!ZEND_TYPE_IS_COMPLEX(type)) {
return ZEND_TYPE_PURE_MASK(type) & (MAY_BE_OBJECT|MAY_BE_ARRAY);
} else if (ZEND_TYPE_IS_UNION(type)) {
zend_type *list_type;
ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), list_type) {
if (zend_type_may_be_cyclic(*list_type)) {
return true;
}
} ZEND_TYPE_LIST_FOREACH_END();
return false;
}

return true;
}

ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, zend_string *name, zval *property, int access_type, zend_string *doc_comment, zend_type type) /* {{{ */
{
zend_property_info *property_info, *property_info_ptr;
Expand All @@ -4506,6 +4542,12 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z
}
}

if (!(access_type & ZEND_ACC_STATIC)
&& !(ce->ce_flags & ZEND_ACC_MAY_BE_CYCLIC)
&& zend_type_may_be_cyclic(type)) {
ce->ce_flags |= ZEND_ACC_MAY_BE_CYCLIC;
}

if (ce->type == ZEND_INTERNAL_CLASS) {
property_info = pemalloc(sizeof(zend_property_info), 1);
} else {
Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_closures.c
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,8 @@ void zend_register_closure_ce(void) /* {{{ */
zend_ce_closure = register_class_Closure();
zend_ce_closure->create_object = zend_closure_new;
zend_ce_closure->default_object_handlers = &closure_handlers;
/* FIXME: Potentially infer ZEND_ACC_MAY_BE_CYCLIC during construction of
* closure? static closures not binding by references can't be cyclic. */
Comment on lines +708 to +709
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 this looks worth it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid my comment was incorrect.

class Test {
    public \Closure $c;
}

$test = new Test();
$c = static function () use ($test) {}; // acyclic
$test->c = $c; // now it's cyclic
unset($test);
gc_collect_cycles();
unset($c); // leaks

This is only ok if $test itself is acyclic. As mentioned previously, this check would currently be unsound because the object may become cyclic at a later point in time by adding dynamic properties. However, once we're on PHP 9, it should be ok!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth mentioning: readonly classes don't permit dynamic properties. So readonly classes could benefit from this optimization immediately without waiting for PHP 9.0.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, your test case doesn't show any dynamic property usage @iluuu1994, wouldn't that still be an issue in non-readonly cases even in PHP 9.0?

Copy link
Member Author

@iluuu1994 iluuu1994 Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth mentioning: readonly classes don't permit dynamic properties. So readonly classes could benefit from this optimization immediately without waiting for PHP 9.0.

I was referring to:

This is only ok if $test itself is acyclic. As mentioned previously, this check would currently be unsound

I.e. it's safe to assume Closure is acyclic only if it captures values that aren't themselves acyclic. If Test may point to cyclic values, as Closure itself, then it won't be marked as acyclic. But today, even if Test were inferrably acyclic, this is not guaranteed to remain this way due to dynamic properties. Hence, we need to be more careful about relations between objects.

Actually, your test case doesn't show any dynamic property usage @iluuu1994, wouldn't that still be an issue in non-readonly cases even in PHP 9.0?

Yes, but it's not a priority since it's a rare case and unlikely to make a big difference.


memcpy(&closure_handlers, &std_object_handlers, sizeof(zend_object_handlers));
closure_handlers.free_obj = zend_closure_free_storage;
Expand Down
5 changes: 4 additions & 1 deletion Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ typedef struct _zend_oparray_context {
#define ZEND_ACC_PROTECTED_SET (1 << 11) /* | | X | */
#define ZEND_ACC_PRIVATE_SET (1 << 12) /* | | X | */
/* | | | */
/* Class Flags (unused: 30,31) | | | */
/* Class Flags (unused: 31) | | | */
/* =========== | | | */
/* | | | */
/* Special class types | | | */
Expand Down Expand Up @@ -333,6 +333,9 @@ typedef struct _zend_oparray_context {
/* Class cannot be serialized or unserialized | | | */
#define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */
/* | | | */
/* Object may be the root of a cycle | | | */
#define ZEND_ACC_MAY_BE_CYCLIC (1 << 30) /* X | | | */
/* | | | */
/* Function Flags (unused: 29-30) | | | */
/* ============== | | | */
/* | | | */
Expand Down
4 changes: 4 additions & 0 deletions Zend/zend_inheritance.c
Original file line number Diff line number Diff line change
Expand Up @@ -1810,6 +1810,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
ce->parent = parent_ce;
ce->default_object_handlers = parent_ce->default_object_handlers;
ce->ce_flags |= ZEND_ACC_RESOLVED_PARENT;
ce->ce_flags |= (parent_ce->ce_flags & ZEND_ACC_MAY_BE_CYCLIC);

/* Inherit properties */
if (parent_ce->default_properties_count) {
Expand Down Expand Up @@ -2832,6 +2833,9 @@ static void zend_do_traits_property_binding(zend_class_entry *ce, zend_class_ent
if (!traits[i]) {
continue;
}

ce->ce_flags |= (traits[i]->ce_flags & ZEND_ACC_MAY_BE_CYCLIC);

ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&traits[i]->properties_info, prop_name, property_info) {
uint32_t flags = property_info->flags;

Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_object_handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ ZEND_API HashTable *rebuild_object_properties_internal(zend_object *zobj) /* {{{
zend_class_entry *ce = zobj->ce;
int i;

GC_TYPE_INFO(zobj) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would break badly if this changed during GC, if the GC used this to decide when to traverse nodes. I suggest to add an assertion here to check that GC is not running.


zobj->properties = zend_new_array(ce->default_properties_count);
if (ce->default_properties_count) {
zend_hash_real_init_mixed(zobj->properties);
Expand Down
3 changes: 3 additions & 0 deletions Zend/zend_objects.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ static zend_always_inline void _zend_object_std_init(zend_object *object, zend_c
{
GC_SET_REFCOUNT(object, 1);
GC_TYPE_INFO(object) = GC_OBJECT;
if (!(ce->ce_flags & ZEND_ACC_MAY_BE_CYCLIC)) {
GC_TYPE_INFO(object) |= (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT);
}
object->ce = ce;
object->extra_flags = 0;
object->handlers = ce->default_object_handlers;
Expand Down
4 changes: 3 additions & 1 deletion Zend/zend_objects_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors(zend_objects_sto
|| obj->ce->destructor) {
GC_ADDREF(obj);
obj->handlers->dtor_obj(obj);
GC_DELREF(obj);
if (UNEXPECTED(GC_DELREF(obj) == 0)) {
zend_objects_store_del(obj);
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions ext/reflection/php_reflection.c
Original file line number Diff line number Diff line change
Expand Up @@ -4343,6 +4343,16 @@ ZEND_METHOD(ReflectionClass, getAttributes)
}
/* }}} */

ZEND_METHOD(ReflectionClass, mayBeCyclic)
{
reflection_object *intern;
zend_class_entry *ce;

GET_REFLECTION_OBJECT_PTR(ce);

RETURN_BOOL(ce->ce_flags & ZEND_ACC_MAY_BE_CYCLIC);
}

/* {{{ Returns the class' constructor if there is one, NULL otherwise */
ZEND_METHOD(ReflectionClass, getConstructor)
{
Expand Down
2 changes: 2 additions & 0 deletions ext/reflection/php_reflection.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ public function getNamespaceName(): string {}
public function getShortName(): string {}

public function getAttributes(?string $name = null, int $flags = 0): array {}

public function mayBeCyclic(): bool {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for exposing this. This may be beneficial for understanding the GC behaviour as PHP user.

}

class ReflectionObject extends ReflectionClass
Expand Down
6 changes: 5 additions & 1 deletion ext/reflection/php_reflection_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion ext/reflection/tests/ReflectionClass_toString_001.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Class [ <internal:Reflection> class ReflectionClass implements Stringable, Refle
Property [ public string $name ]
}

- Methods [64] {
- Methods [65] {
Method [ <internal:Reflection> private method __clone ] {

- Parameters [0] {
Expand Down Expand Up @@ -514,5 +514,12 @@ Class [ <internal:Reflection> class ReflectionClass implements Stringable, Refle
}
- Return [ array ]
}

Method [ <internal:Reflection> public method mayBeCyclic ] {

- Parameters [0] {
}
- Return [ bool ]
}
}
}
Loading