-
Notifications
You must be signed in to change notification settings - Fork 7.9k
GC optimization for primitive arrays #19626
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -983,6 +983,9 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) | |
handle_ht: | ||
n = ht->nNumUsed; | ||
zv = ht->arPacked; | ||
if (GC_FLAGS(ht) & GC_NOT_COLLECTABLE) { | ||
goto next; | ||
} | ||
if (HT_IS_PACKED(ht)) { | ||
goto handle_zvals; | ||
} | ||
|
@@ -1041,7 +1044,7 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) | |
* counts and mark visited nodes grey. See MarkGray() in Bacon & Rajan. */ | ||
static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) | ||
{ | ||
HashTable *ht; | ||
HashTable *ht, *primitive_ht = NULL; | ||
Bucket *p; | ||
zval *zv; | ||
uint32_t n; | ||
|
@@ -1132,6 +1135,7 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) | |
handle_zvals: | ||
for (; n != 0; n--) { | ||
if (Z_COLLECTABLE_P(zv)) { | ||
primitive_ht = NULL; | ||
ref = Z_COUNTED_P(zv); | ||
GC_DELREF(ref); | ||
if (!GC_REF_CHECK_COLOR(ref, GC_GREY)) { | ||
|
@@ -1153,12 +1157,20 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) | |
} | ||
zv++; | ||
} | ||
if (primitive_ht) { | ||
GC_ADD_FLAGS(primitive_ht, GC_NOT_COLLECTABLE); | ||
primitive_ht = NULL; | ||
} | ||
} | ||
} else if (GC_TYPE(ref) == IS_ARRAY) { | ||
ZEND_ASSERT(((zend_array*)ref) != &EG(symbol_table)); | ||
ht = (zend_array*)ref; | ||
handle_ht: | ||
n = ht->nNumUsed; | ||
if (GC_FLAGS(ht) & GC_NOT_COLLECTABLE) { | ||
goto next; | ||
} | ||
primitive_ht = ht; | ||
Comment on lines
+1170
to
+1173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be moved above #[AllowDynamicProperties]
class C {
public $c;
}
$c = new C();
$c->c = 1;
$c->d = 2; // dynamic prop
$x = $c;
unset($x); // add to roots
gc_collect_cycles(); // marks C->properties as not collectable
$c->c = $c; // should be collectable now, but isn't
unset($c);
gc_collect_cycles(); // ignores C->properties Otherwise, this looks right to me There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, very good catch. |
||
if (HT_IS_PACKED(ht)) { | ||
zv = ht->arPacked; | ||
goto handle_zvals; | ||
|
@@ -1171,6 +1183,7 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) | |
zv = Z_INDIRECT_P(zv); | ||
} | ||
if (Z_COLLECTABLE_P(zv)) { | ||
primitive_ht = NULL; | ||
ref = Z_COUNTED_P(zv); | ||
GC_DELREF(ref); | ||
if (!GC_REF_CHECK_COLOR(ref, GC_GREY)) { | ||
|
@@ -1196,6 +1209,10 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) | |
} | ||
p++; | ||
} | ||
if (primitive_ht) { | ||
GC_ADD_FLAGS(ht, GC_NOT_COLLECTABLE); | ||
primitive_ht = NULL; | ||
} | ||
} else if (GC_TYPE(ref) == IS_REFERENCE) { | ||
if (Z_COLLECTABLE(((zend_reference*)ref)->val)) { | ||
ref = Z_COUNTED(((zend_reference*)ref)->val); | ||
|
@@ -1378,6 +1395,9 @@ static void gc_scan(zend_refcounted *ref, gc_stack *stack) | |
|
||
handle_ht: | ||
n = ht->nNumUsed; | ||
if (GC_FLAGS(ht) & GC_NOT_COLLECTABLE) { | ||
goto next; | ||
} | ||
if (HT_IS_PACKED(ht)) { | ||
zv = ht->arPacked; | ||
goto handle_zvals; | ||
|
@@ -1623,6 +1643,9 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta | |
|
||
handle_ht: | ||
n = ht->nNumUsed; | ||
if (GC_FLAGS(ht) & GC_NOT_COLLECTABLE) { | ||
goto next; | ||
} | ||
if (HT_IS_PACKED(ht)) { | ||
zv = ht->arPacked; | ||
goto handle_zvals; | ||
|
@@ -1811,6 +1834,9 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe | |
|
||
handle_ht: | ||
n = ht->nNumUsed; | ||
if (GC_FLAGS(ht) & GC_NOT_COLLECTABLE) { | ||
goto next; | ||
} | ||
if (HT_IS_PACKED(ht)) { | ||
zv = ht->arPacked; | ||
goto handle_zvals; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -885,6 +885,7 @@ static zend_always_inline zval *_zend_hash_add_or_update_i(HashTable *ht, zend_s | |
nIndex = h | ht->nTableMask; | ||
Z_NEXT(p->val) = HT_HASH_EX(arData, nIndex); | ||
HT_HASH_EX(arData, nIndex) = HT_IDX_TO_HASH(idx); | ||
GC_DEL_FLAGS(ht, GC_NOT_COLLECTABLE); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Am I right this will delete the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a trade-off. The patch is already adding 0.05% instructions to Wordpress. A branch with a potential for a branch miss may not be worth it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please let's have the impact tested. GC on large array is really costly and appending to a large array should ideally imply no single GC run when not really needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be difficult to track that precisely due to the way arrays are updated. For example, the statement Could you tell more about the use-case @mvorisek @dktapps? One alternative would be something like JS TypedArrays: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray. This would be GC friendly, space efficient, JIT friendly, and would solve some use-cases better than arrays or strings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use PHP for CLI heavily, very often we cache large datasets (database data, CSV data, ...) with memory frequently over 5 GB. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given you also created GH-17131, this PR will not be sufficient either. It's unlikely that there's a silver bullet to this problem. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current optimization would probably already help a lot for workflows in which the GC spends a lot of a time scanning scalar arrays that are usually not updated between two GC runs. @dktapps @mvorisek would you be able to measure the impact on your code? In addition to that, a simple generational GC scheme would help as well, for use-cases with long lived large data sets:
I will try to implement the generational GC idea.
Arrays with a size different than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well, when digging into the problem in my specific case, I found that blacklisting objects containing large arrays dropped my average GC from 22ms to 4ms, so that's a pretty big W in my book. (Also worth noting, my application runs on a 20Hz tick cycle, so this makes a major improvement to game tick lag.) I feel like this is a much easier win than generational GC. My tests used this extension: https://github.com/dktapps/ext-gcignore (with some local changes) that flags vars as GC_NOT_COLLECTABLE, which is basically what this PR is doing. So it's a fairly good analog. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I see you responded to a different message than I thought. I thought you were responding to the explicit opt-out of GC for a single value, e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@iluuu1994 I already explored explicit opt-out ability with my extension and found it to be clunky. It works, but having to splash The more sustainable solutions to the arrays issue would be a new GC (generational seems to be the favourite, there might be other options that I'm not aware of), or this PR. While this PR wouldn't generally solve the issue of long-lived objects being repeatedly scanned, it would (at least in my case) dramatically reduce the scale of the performance impact, with what seems like a relatively easy change. |
||
if (flag & HASH_LOOKUP) { | ||
ZVAL_NULL(&p->val); | ||
} else { | ||
|
@@ -970,6 +971,7 @@ static zend_always_inline zval *_zend_hash_str_add_or_update_i(HashTable *ht, co | |
nIndex = h | ht->nTableMask; | ||
Z_NEXT(p->val) = HT_HASH(ht, nIndex); | ||
HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(idx); | ||
GC_DEL_FLAGS(ht, GC_NOT_COLLECTABLE); | ||
|
||
return &p->val; | ||
} | ||
|
@@ -1172,6 +1174,7 @@ static zend_always_inline zval *_zend_hash_index_add_or_update_i(HashTable *ht, | |
p = ht->arData + idx; | ||
Z_NEXT(p->val) = HT_HASH(ht, nIndex); | ||
HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(idx); | ||
GC_DEL_FLAGS(ht, GC_NOT_COLLECTABLE); | ||
if ((zend_long)h >= ht->nNextFreeElement) { | ||
ht->nNextFreeElement = (zend_long)h < ZEND_LONG_MAX ? h + 1 : ZEND_LONG_MAX; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
empty array should be
GC_NOT_COLLECTABLE
from start