array/HashTable filling optimization

php.internals

Matt W

19 years ago
Hi all, I noticed awhile ago how most every use of zend[_u]_hash_init has nSize as 0. Of course it isn't always known how many elements will be added to the array (nTableSize), but there are places where an accurate value could be used instead of getting the minimum default (8). For anyone who doesn't know, HashTable sizes are a power of 2, and when there are more than that many elements, zend_hash_do_resize realloc's more memory to double the table size, and then zend_hash_rehash basically goes through and "reinserts" all the elements again. I ran some tests to see the speed improvement from specifying the *actual* number of elements in hash_init, thus saving that extra work (_rehash mostly). The "n+1" is with one more element (9, 17, 33...) that triggers another rehash when the actual number wasn't specified (most extreme). (I used add_next_index_zval, so the direct zend_hash* functions would give slightly higher % I guess, being less overhead, right?) Results with 5.2.0-RC4: Elements n | n n+1 --------------------- 8 | 0% 14.2% 16 | 11.0% 20.9% 32 | 13.5% 22.1% 64 | 12.6% 21.3% 128 | 11.7% 18.5% 256 | 9.3% 16.4% 512 | 8.6% 17.0% 1024 | 7.9% 15.8% 2048 | 4.8% 33.3% 4096 | 7.8% 28.3% 8192 | 10.2% 58.4% 16384 | 24.1% 70.5% 32768 | 34.5% 80.4% 65536 | 34.8% 68.6% I haven't looked thoroughly, but the only place I've seen that uses an non-zero nSize in hash_init (besides some in the core engine) is the unserialize() function. :-/ It seems the most simple and obvious place that could be changed is in Zend/zend_variables.c:_zval_copy_ctor_func? Just replace zend[_u]_hash_init(tmp_ht, 0, ... with zend[_u]_hash_init(tmp_ht, original_ht->nNumOfElements, ... Other than that, some of PHP's array functions and array-returning functions (database fetch row functions, etc.) are ones I thought of that could be optimized like this. Maybe create an array_init_size() macro to be used in places where the number of elements can be easily determined? Thoughts? I'd volunteer to look for places that could be updated and modify them. :-) Thanks, Matt P.S. I guess the array stuff applies for objects also? But I don't know much about their internals...

Nuno Lopes

19 years ago
Aaahh nice catch ;) From a quick lookup in PHP sources I also found these functions were the same optimization can be applied: zend_default_exception_new_ex() - trivial convert_to_array() - not as easy as others zend_object_std_init() - possibly clone_wrapper_hash() - trivial (php-src/main/streams/streams.c) compiler_globals_ctor() ... Nuno ----- Original Message -----

Matt W

19 years ago
Hi Nuno, Late reply, but I'm glad the idea was able to be used. :-) To the other hash people: While checking how Ilia made implode() faster in 5.2, I think I found another optimization that can be done. In zend_hash_copy/merge, it seems the zend_hash_quick_* functions can be used instead for associative entries, thus saving the key from being hashed again, right? I just checked, and it's definitely faster, how much so being dependent on the key length of course... Patches for this simple additional optimization (hopefully): http://realplain.com/php/hash_quick_copy.diff http://realplain.com/php/hash_quick_copy_5_2.diff Thanks, Matt ----- Original Message ----- From: "Nuno Lopes" Sent: Wednesday, September 27, 2006
> Aaahh nice catch ;) > > >From a quick lookup in PHP sources I also found these functions were the > same optimization can be applied: > zend_default_exception_new_ex() - trivial > convert_to_array() - not as easy as others > zend_object_std_init() - possibly > clone_wrapper_hash() - trivial (php-src/main/streams/streams.c) > compiler_globals_ctor() > ... > > > Nuno > > > ----- Original Message ----- > > Hi all, > > > > I noticed awhile ago how most every use of zend[_u]_hash_init has nSize
as
> > 0. Of course it isn't always known how many elements will be added to
the
> > array (nTableSize), but there are places where an accurate value could
be
> > used instead of getting the minimum default (8). For anyone who doesn't > > know, HashTable sizes are a power of 2, and when there are more than
that
> > many elements, zend_hash_do_resize realloc's more memory to double the > > table > > size, and then zend_hash_rehash basically goes through and "reinserts"
all
> > the elements again. > > > > I ran some tests to see the speed improvement from specifying the
*actual*
> > number of elements in hash_init, thus saving that extra work (_rehash > > mostly). The "n+1" is with one more element (9, 17, 33...) that
triggers
> > another rehash when the actual number wasn't specified (most extreme).
(I
> > used add_next_index_zval, so the direct zend_hash* functions would give > > slightly higher % I guess, being less overhead, right?) Results with > > 5.2.0-RC4: > > > > Elements > > n | n n+1 > > --------------------- > > 8 | 0% 14.2% > > 16 | 11.0% 20.9% > > 32 | 13.5% 22.1% > > 64 | 12.6% 21.3% > > 128 | 11.7% 18.5% > > 256 | 9.3% 16.4% > > 512 | 8.6% 17.0% > > 1024 | 7.9% 15.8% > > 2048 | 4.8% 33.3% > > 4096 | 7.8% 28.3% > > 8192 | 10.2% 58.4% > > 16384 | 24.1% 70.5% > > 32768 | 34.5% 80.4% > > 65536 | 34.8% 68.6% > > > > I haven't looked thoroughly, but the only place I've seen that uses an > > non-zero nSize in hash_init (besides some in the core engine) is the > > unserialize() function. :-/ It seems the most simple and obvious place > > that > > could be changed is in Zend/zend_variables.c:_zval_copy_ctor_func? Just > > replace > > > > zend[_u]_hash_init(tmp_ht, 0, ... > > with > > zend[_u]_hash_init(tmp_ht, original_ht->nNumOfElements, ... > > > > Other than that, some of PHP's array functions and array-returning > > functions > > (database fetch row functions, etc.) are ones I thought of that could be > > optimized like this. Maybe create an array_init_size() macro to be used > > in > > places where the number of elements can be easily determined? Thoughts? > > I'd volunteer to look for places that could be updated and modify them. > > :-) > > > > > > Thanks, > > Matt > > > > P.S. I guess the array stuff applies for objects also? But I don't
know

Ilia A.

19 years ago
Looks like a good optimization to me. On 5-Nov-06, at 7:30 AM, Matt Wilmas wrote:
> Hi Nuno, > > Late reply, but I'm glad the idea was able to be used. :-) > > > To the other hash people: While checking how Ilia made implode() > faster in > 5.2, I think I found another optimization that can be done. In > zend_hash_copy/merge, it seems the zend_hash_quick_* functions can > be used > instead for associative entries, thus saving the key from being hashed > again, right? I just checked, and it's definitely faster, how much > so being > dependent on the key length of course... > > Patches for this simple additional optimization (hopefully): > http://realplain.com/php/hash_quick_copy.diff > http://realplain.com/php/hash_quick_copy_5_2.diff > > > Thanks, > Matt > > > ----- Original Message ----- > From: "Nuno Lopes" > Sent: Wednesday, September 27, 2006 > >> Aaahh nice catch ;) >> >>> From a quick lookup in PHP sources I also found these functions >>> were the >> same optimization can be applied: >> zend_default_exception_new_ex() - trivial >> convert_to_array() - not as easy as others >> zend_object_std_init() - possibly >> clone_wrapper_hash() - trivial (php-src/main/streams/streams.c) >> compiler_globals_ctor() >> ... >> >> >> Nuno >> >> >> ----- Original Message ----- >>> Hi all, >>> >>> I noticed awhile ago how most every use of zend[_u]_hash_init has >>> nSize > as >>> 0. Of course it isn't always known how many elements will be >>> added to > the >>> array (nTableSize), but there are places where an accurate value >>> could > be >>> used instead of getting the minimum default (8). For anyone who >>> doesn't >>> know, HashTable sizes are a power of 2, and when there are more than > that >>> many elements, zend_hash_do_resize realloc's more memory to >>> double the >>> table >>> size, and then zend_hash_rehash basically goes through and >>> "reinserts" > all >>> the elements again. >>> >>> I ran some tests to see the speed improvement from specifying the > *actual* >>> number of elements in hash_init, thus saving that extra work >>> (_rehash >>> mostly). The "n+1" is with one more element (9, 17, 33...) that > triggers >>> another rehash when the actual number wasn't specified (most >>> extreme). > (I >>> used add_next_index_zval, so the direct zend_hash* functions >>> would give >>> slightly higher % I guess, being less overhead, right?) Results >>> with >>> 5.2.0-RC4: >>> >>> Elements >>> n | n n+1 >>> --------------------- >>> 8 | 0% 14.2% >>> 16 | 11.0% 20.9% >>> 32 | 13.5% 22.1% >>> 64 | 12.6% 21.3% >>> 128 | 11.7% 18.5% >>> 256 | 9.3% 16.4% >>> 512 | 8.6% 17.0% >>> 1024 | 7.9% 15.8% >>> 2048 | 4.8% 33.3% >>> 4096 | 7.8% 28.3% >>> 8192 | 10.2% 58.4% >>> 16384 | 24.1% 70.5% >>> 32768 | 34.5% 80.4% >>> 65536 | 34.8% 68.6% >>> >>> I haven't looked thoroughly, but the only place I've seen that >>> uses an >>> non-zero nSize in hash_init (besides some in the core engine) is the >>> unserialize() function. :-/ It seems the most simple and obvious >>> place >>> that >>> could be changed is in Zend/ >>> zend_variables.c:_zval_copy_ctor_func? Just >>> replace >>> >>> zend[_u]_hash_init(tmp_ht, 0, ... >>> with >>> zend[_u]_hash_init(tmp_ht, original_ht->nNumOfElements, ... >>> >>> Other than that, some of PHP's array functions and array-returning >>> functions >>> (database fetch row functions, etc.) are ones I thought of that >>> could be >>> optimized like this. Maybe create an array_init_size() macro to >>> be used >>> in >>> places where the number of elements can be easily determined? >>> Thoughts? >>> I'd volunteer to look for places that could be updated and modify >>> them. >>> :-) >>> >>> >>> Thanks, >>> Matt >>> >>> P.S. I guess the array stuff applies for objects also? But I don't > know >>> much about their internals... > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: http://www.php.net/unsub.php > >
Ilia Alshanetsky

Dmitry Stogov

19 years ago
The patch is fine. I applied it. Dmitry.