Skip to content

Optimize SplFixedArray when magic methods aren't overridden #6552

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

Closed
wants to merge 1 commit into from

Conversation

TysonAndre
Copy link
Contributor

@TysonAndre TysonAndre commented Dec 29, 2020

This decreases the memory usage of SplFixedArrays by 32 bytes per object
on 64-bit systems (use 1 null pointer instead of 5 null pointers)
If allocating a lot of arrays of size 1, memory usage was 19.44MiB before this
change, and 16.24MiB after the change.

Existing tests continue to pass.

Subclassing SplFixedArray is already inefficient and rarely done - it checks for the existence of 5 methods every time a subclass is instantiated.
(and has to switch back from C to the php vm to call those methods)

<?php
/*
Before(last run)
Change to memory in SplFixedArray: 79.124MiB elapsed: 0.114
Change to memory in array:        204.781MiB elapsed: 0.076

After optimizations (last run)
Change to memory in SplFixedArray: 63.124MiB elapsed: 0.098
Change to memory in array:        204.781MiB elapsed: 0.075

 */
ini_set('memory_limit', '2G');
function bench() {
    call_user_func(function () {
        gc_collect_cycles();
        $start = microtime(true);
        $count = 500000;
        $old_memory = memory_get_usage();
        $values = new SplFixedArray($count);
        for ($i = 0; $i < $count; $i++) {
            $values[$i] = new SplFixedArray(1);
            $values[$i]->offsetSet(0, $i);
        }
        $new_memory = memory_get_usage();
        unset($values);
        $end = microtime(true);
        printf("Change to memory in SplFixedArray: %.3fMiB elapsed: %.3f\n", ($new_memory - $old_memory)/1000000, $end-$start);
    });
    call_user_func(function () {
        gc_collect_cycles();
        $start = microtime(true);
        $count = 500000;
        $old_memory = memory_get_usage();
        $values = [];
        for ($i = 0; $i < $count; $i++) {
            $values[$i] = [$i];
        }
        $new_memory = memory_get_usage();
        unset($values);
        $end = microtime(true);
        printf("Change to memory in array:        %.3fMiB elapsed: %.3f\n", ($new_memory - $old_memory)/1000000, $end-$start);
    });
}
for ($i = 0; $i < 10; $i++) {
    bench();
}

This decreases the memory usage of SplFixedArrays by 32 bytes per object
on 64-bit systems (use 1 null pointer instead of 5 null pointers)
If allocating a lot of arrays of size 1, memory usage was 19.44MiB before this
change, and 16.24MiB after the change.

Existing tests continue to pass.

Subclassing SplFixedArray is already inefficient and rarely done.
It checks for the existence of 5 methods every time a subclass is instantiated.
(and has to switch back from C to the php vm to call those methods)
@TysonAndre
Copy link
Contributor Author

@morrisonlevi @nikic @kocsismate - any objections?

@morrisonlevi
Copy link
Contributor

You reduce the size of each SplFixedArray by 32 bytes, yet in the script comments it says after applying this optimization you went from 79.124MiB down to 63.124MiB, yet it only instantiates 10 SplFixedArrays. What am I missing?

@TysonAndre
Copy link
Contributor Author

TysonAndre commented Jan 2, 2021

There's $count = 500000 arrays being created inside the test. 0.5MiB * 32 is 16MiB, which is what's being reported by the calculation

I ran the benchmark as a whole 10 times in the loop at the bottom

@morrisonlevi
Copy link
Contributor

Ah, I see. I'm not sure how common it is to instantiate more than one at a time.

Pro: obviously it is more space efficient when SplFixedArray isn't inherited from (common).
Con: if you do inherit, it does more allocations. We can't pack it into the same allocation dynamically because SplFixedArray can have dynamic properties and can be inherited from, so unless we can pack it into an official zval (weird) then I don't see an easy way around it, even though we only really need at most one allocation per inherited class.

I will support whatever Nikita or other SPL contributors think is best, as I don't feel strongly about it.

Copy link
Member

@nikic nikic left a comment

Choose a reason for hiding this comment

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

Ideally these methods would be cached in the class_entry, but that's a larger change.


typedef struct _spl_fixedarray_object {
spl_fixedarray array;
spl_fixedarray_methods* methods;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
spl_fixedarray_methods* methods;
spl_fixedarray_methods *methods;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in linked commit

@php-pulls php-pulls closed this in a25886d Jan 4, 2021
@TysonAndre
Copy link
Contributor Author

Ideally these methods would be cached in the class_entry, but that's a larger change.

True - I wasn't sure if there was already a way to automatically run a C callback to modify any subclass of a class such as SplFixedArray but it didn't seem straightforward.

Using two sets of handlers and different sized allocations to avoid checking ->methods in the base class seemed possible, but harder to maintain

@TysonAndre
Copy link
Contributor Author

I'd misunderstood what you meant earlier.

	/* allocated only if class implements Iterator or IteratorAggregate interface */
	zend_class_iterator_funcs *iterator_funcs_ptr;

If a new arraylike_funcs_ptr were added to php for ArrayAccess and handled when a class implemented ArrayAccess, instance->ce->arrayaccess_funcs_ptr->offsetget->scope != my_base_class_entry could be used to check if the internal implementation was overridden. Is that what you mean?

zend_ce_iterator->interface_gets_implemented = zend_implement_iterator; exists for the Iterator interface, and this would be something similar for ArrayAccess? (instance->ce->arrayaccess_funcs_ptr->offsetget->scope != my_base_class_entry could be used to check if the internal implementation was overridden)

TysonAndre added a commit to TysonAndre/php-src that referenced this pull request Dec 2, 2021
Previously, code such as subclasses of SplFixedArray would check for method
overrides when instantiating the objects.

This optimization was mentioned as a followup to phpGH-6552
TysonAndre added a commit to TysonAndre/php-src that referenced this pull request Dec 4, 2021
Previously, code such as subclasses of SplFixedArray would check for method
overrides when instantiating the objects.

This optimization was mentioned as a followup to phpGH-6552
TysonAndre added a commit that referenced this pull request Dec 4, 2021
Previously, code such as subclasses of SplFixedArray would check for method
overrides when instantiating the objects.

This optimization was mentioned as a followup to GH-6552
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants