Smart strings & memory allocation

php.internals

Stanislav Malyshev

23 years ago
Doing some tests regarding memory allocation, I have discovered that the implementation of smart strings (php_smart_str.h), user in spprintf and everything above it, is kind of sub-optimal with regard to memory allocation. That's why: The initial string buffer is allocated of size at least 128 bytes. This leads to the effect that the allocation is not served by Zend malloc cache, but each time is malloced from the system. Also, the size of 128 (for which 148 bytes is really allocated due to various rounding and overheads) is way too big, the final size of 90% strings in real tests is below 80 bytes (I did the testing on 'make test', but I guess the most other applications behave the similiar way). So, my proposal is to make initial allocation of smart string memory block of 79 bytes. This number is not round on purpose - so it fits the 80-byte cache block, which is currently the biggest cached memory block. The reallocation can be done in 128-byte incerements or any other increments, the tests show 'big' sizes show no obvious pattern to make any incerement better than others. People who wrote this code (I guess that would be Sacha, but maybe others too) are especially welcome to comment on this, and anybody else who has something to say on the matter is welcome either. The patch is below: --- php_smart_str.h 10 Jun 2003 20:03:38 -0000 1.26 +++ php_smart_str.h 18 Jun 2003 08:34:20 -0000 @@ -36,6 +36,10 @@ #define SMART_STR_PREALLOC 128 #endif +#ifndef SMART_STR_START_SIZE +#define SMART_STR_START_SIZE 78 +#endif + #ifdef SMART_STR_USE_REALLOC #define SMART_STR_REALLOC(a,b,c) realloc((a),(b)) #else @@ -47,7 +51,11 @@ if (!(d)->c) (d)->len = (d)->a = 0; \ newlen = (d)->len + (n); \ if (newlen >= (d)->a) { \ + if((d)->a == 0 && newlen < SMART_STR_START_SIZE) { \ + (d)->a = SMART_STR_START_SIZE; \ + } else { \ (d)->a = newlen + SMART_STR_PREALLOC; \ + } \ (d)->c = SMART_STR_REALLOC((d)->c, (d)->a + 1, (what)); \ } \ } while (0)
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Sascha Schumann

23 years ago
Fine with the concept. If you move the additional check (newlen < SMART_STR_START_SIZE) and preallocation into the if (!(d)->c) branch, the changes won't affect the common code path.
> @@ -47,7 +51,11 @@ > if (!(d)->c) (d)->len = (d)->a = 0; \ > newlen = (d)->len + (n); \ > if (newlen >= (d)->a) { \ > + if((d)->a == 0 && newlen < SMART_STR_START_SIZE) { \ > + (d)->a = SMART_STR_START_SIZE; \ > + } else { \ > (d)->a = newlen + SMART_STR_PREALLOC; \ > + } \ > (d)->c = SMART_STR_REALLOC((d)->c, (d)->a + 1, (what)); \ > } \ > } while (0)
- Sascha

Stanislav Malyshev

23 years ago
SS>> If you move the additional check (newlen < SS>> SMART_STR_START_SIZE) and preallocation into the if (!(d)->c) SS>> branch, the changes won't affect the common code path. IMO, the common code path is that of SMART_STR_START_SIZE, since as I said 90% of strings never reach beyond initial alloc. But I see your point, you are right, the START_SIZE one should be into if (!(d)->c) branch. SS>> SS>> > @@ -47,7 +51,11 @@ SS>> > if (!(d)->c) (d)->len = (d)->a = 0; \ SS>> > newlen = (d)->len + (n); \ SS>> > if (newlen >= (d)->a) { \ SS>> > + if((d)->a == 0 && newlen < SMART_STR_START_SIZE) { \ SS>> > + (d)->a = SMART_STR_START_SIZE; \ SS>> > + } else { \ SS>> > (d)->a = newlen + SMART_STR_PREALLOC; \ SS>> > + } \ SS>> > (d)->c = SMART_STR_REALLOC((d)->c, (d)->a + 1, (what)); \ SS>> > } \ SS>> > } while (0) SS>> SS>> - Sascha SS>> SS>>
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Sascha Schumann

23 years ago
On Wed, 18 Jun 2003, Stanislav Malyshev wrote:
> SS>> If you move the additional check (newlen < > SS>> SMART_STR_START_SIZE) and preallocation into the if (!(d)->c) > SS>> branch, the changes won't affect the common code path. > > IMO, the common code path is that of SMART_STR_START_SIZE, since as I > said 90% of strings never reach beyond initial alloc. But I see your > point, you are right, the START_SIZE one should be into if (!(d)->c) > branch.
Common referred to real applications, not 'make test'. In fact, whenever strings are not allocated through the engine, 80 bytes is a really poor choice, because the malloc implementation will very likely allocate a 128 byte chunk anyway. - Sascha (who just yesterday ported FreeBSD's malloc.c for shared memory handling)

Stanislav Malyshev

23 years ago
SS>> Common referred to real applications, not 'make test'. Do you have some data about real applications in PHP that use strings longer than 80 bytes via smart strings? My tests show most of the strings used are small. Looking at the clients of this functions and taking into account that prontf's are usually small and long printf are usually or rewritten into heredocs (which don't use smart strings) or separated into different functions. BTW, why do you think "make test" is a bad test of memory allocation patterns? What do you propose as a better test? SS>> In fact, whenever strings are not allocated through the SS>> engine, 80 bytes is a really poor choice, because the malloc SS>> implementation will very likely allocate a 128 byte chunk SS>> anyway. That's very bad news, because actually my test show that about 95% of mallocs are for sizes below 100 bytes. But I hope the system malloc has some mechanism to deal with it, otherwise we are wasting a real lot of memory. Actually, given that tests show 90% of mallocs are for sizes below 64 bytes, if what you say is right, PHP's memory footprint is at least twice as big as it is meant to be. The number 80 was chosen because even if malloc allocates 128 bytes, these 128 bytes would be reused using the Zend memory cache. However, if the size of the segment does not allow it to be into the Zend memory cache, new malloc would be attempted each time. That was the primery rationale for the change, not the memory-saving argument, though it is important too. BTW, the number 128 in the code allocated in fact at least 148 bytes, and I don't know what malloc implemetation would do of this.
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Per Lundberg

23 years ago
On Wed, 2003-06-18 at 15:21, Stanislav Malyshev wrote:
> The number 80 was chosen because even if malloc allocates 128 bytes, these > 128 bytes would be reused using the Zend memory cache. However, if the > size of the segment does not allow it to be into the Zend memory cache, > new malloc would be attempted each time. That was the primery rationale > for the change, not the memory-saving argument, though it is important > too.
How about including a malloc() implementation of your own? Doug Lea's dmalloc comes to mind. That way, you should be able to get a much higher level of control over how it allocates small blocks (which are indeed very common in almost all computer programs).
-- Best regards, Per Lundberg / Capio ApS Phone: +46-18-4186040 Fax: +46-18-4186049 Web: http://www.nobolt.com

Stanislav Malyshev

23 years ago
PL>> How about including a malloc() implementation of your own? Doug PL>> Lea's dmalloc comes to mind. That way, you should be able to get a PL>> much higher level of control over how it allocates small blocks PL>> (which are indeed very common in almost all computer programs). In fact, that was initial though behind all my tests - to see how many small blocks PHP allocates, what are these small blocks and how well they are cached. If anybody is interested in working or it or has suggestions about that - I can share my thoughts. BTW, I don't exactly think dmalloc is the best way for what we are going to do, since AFAIK it's general-purpose allocator, and what we need is more to the direction of fixed-size block allocator.
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Per Lundberg

23 years ago
On Wed, 2003-06-18 at 15:41, Stanislav Malyshev wrote:
> BTW, I don't exactly think dmalloc is the best way for what we are going > to do, since AFAIK it's general-purpose allocator, and what we need is > more to the direction of fixed-size block allocator.
Fixed-size? Do you mean an allocator that is optimized for allocation of blocks of a certain size, or an allocator that can only allocate blocks of a certain size? I have only written a couple of allocators, and the best one I did used fixed block sizes, such as 16 bytes, 32 bytes, 64 bytes and so on... I suppose that's the way dlmalloc works as well, and that's the problem.
-- Best regards, Per Lundberg / Capio ApS Phone: +46-18-4186040 Fax: +46-18-4186049 Web: http://www.nobolt.com

Stanislav Malyshev

23 years ago
PL>> Fixed-size? Do you mean an allocator that is optimized for PL>> allocation of blocks of a certain size, or an allocator that can only PL>> allocate blocks of a certain size? The former, which can be the latter too, since we can always fall back to malloc.
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Per Lundberg

23 years ago
On Wed, 2003-06-18 at 16:11, Stanislav Malyshev wrote:
> PL>> Fixed-size? Do you mean an allocator that is optimized for > PL>> allocation of blocks of a certain size, or an allocator that can only > PL>> allocate blocks of a certain size? > The former, which can be the latter too, since we can always fall back to > malloc.
This might actually be a very good idea, since it can likely improve performance a lot by having a pool of zval-sized blocks (I have no idea of how big they are). Have you thought about how to implement it in a good way?
-- Best regards, Per Lundberg / Capio ApS Phone: +46-18-4186040 Fax: +46-18-4186049 Web: http://www.nobolt.com

Sascha Schumann

23 years ago
> In fact, that was initial though behind all my tests - to see how many > small blocks PHP allocates, what are these small blocks and how well they > are cached. If anybody is interested in working or it or has suggestions > about that - I can share my thoughts.
If you are concerned about PHP's memory footprint, then relying on your own memory management makes matters only worse. Why? It would effectively lock up memory in a PHP-specific area, so that other 3rd party software (web server, libraries) won't have access to unused resources anymore. - Sascha

Stanislav Malyshev

23 years ago
SS>> If you are concerned about PHP's memory footprint, then SS>> relying on your own memory management makes matters only SS>> worse. SS>> SS>> Why? It would effectively lock up memory in a PHP-specific SS>> area, so that other 3rd party software (web server, SS>> libraries) won't have access to unused resources anymore. That's right. However, I am not sure if it is worse of better. Actually, I am concerned with both performance (which malloc is known to hurt - malloc is pretty costy operation AFAIK, that's why we have memory cache) and memory footprint. Of those, I am concerned with the former much more than the latter. However, if we have 640% overhead on each zval allocation, this starts to bother me more - because maybe if those 640% aren't available to 3rd-party anyway, so why not make them available at least for ourselves? And maybe we could do even with less than 640%... Again, I do not propose to make _all_ mallocs use spearate space. I'm thinking along the lines of improving those mallocs that are most frequent, most traceable and identifiable. The rest can be happy with mallocs.
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Sascha Schumann

23 years ago
> BTW, why do you think "make test" is a bad test of memory allocation > patterns? What do you propose as a better test?
make test is synthetic. My primary use of smart_str involves IRCG based application.
> That's very bad news, because actually my test show that about 95% of > mallocs are for sizes below 100 bytes. But I hope the system malloc has > some mechanism to deal with it, otherwise we are wasting a real lot of > memory. Actually, given that tests show 90% of mallocs are for sizes below > 64 bytes, if what you say is right, PHP's memory footprint is at least > twice as big as it is meant to be.
Most applications use smart_str for temporarily creating some representation, processing it quickly, and freeing it immediately afterwards. I.e. the overhead in memory allocation is not long term. IRCG is a good example here; it can process GB of IRC data using a 6MB shared memory area. Did you perform any benchmarks and if so, on which OS? Did you try the same test with disabled memory cache?
> The number 80 was chosen because even if malloc allocates 128 bytes, these > 128 bytes would be reused using the Zend memory cache. However, if the > size of the segment does not allow it to be into the Zend memory cache, > new malloc would be attempted each time. That was the primery rationale > for the change, not the memory-saving argument, though it is important > too.
Most malloc implementations are tuned for the repetitive malloc/free pattern of the same size. For those, using the engine's memory cache will not be an advantage, because it prevents memory from being reused through ordinary malloc.
> BTW, the number 128 in the code allocated in fact at least 148 bytes, and > I don't know what malloc implemetation would do of this.
That's pretty bad indeed. - Sascha

Stanislav Malyshev

23 years ago
SS>> make test is synthetic. My primary use of smart_str involves SS>> IRCG based application. Which tells what? What are sizes you use there in IRCG? Anyway, is IRCG the most used client of smart strings? Since much more used printf functions use it too, I guess that with all due respect to IRCG it is not is not the primary user of this code. Thus, I don't think IRCG application is a good way to test system-wide function. But again, I am eager to see an example of general test that you think are better than make test. I chose make test as an example that uses a lot of different functions so my tests would not be aligned against one application's memory usage pattern. SS>> Most applications use smart_str for temporarily creating some SS>> representation, processing it quickly, and freeing it SS>> immediately afterwards. I.e. the overhead in memory Bingo. That's why caching it is so important - we can get rid of a lot of malloc() calls. If you can do one malloc instead of 1000 mallocs - that's very good in my book. But in the paragraph above I was talking about general [e]malloc, not about smart strings, though with smart strings it shows up too. SS>> Most malloc implementations are tuned for the repetitive SS>> malloc/free pattern of the same size. For those, using the SS>> engine's memory cache will not be an advantage, because it SS>> prevents memory from being reused through ordinary malloc. There's almost no 'ordinary malloc' in the engine. All mallocs go through emalloc. Actually, all mallocs should go through emalloc. Also, I would like more explanation about how exactly caching memory allocations can hurt performance - did you try to disable Zend memory cache and actually got better results that with it? Also, mallocs usually means multithreaded locks. Which, as I heard, is Very Bad (TM) for multithreaded performance. However, since ZE memory caches are per-thread, cached allocator does not need any locks. I know only one downside of the memory cache that can really hurt - it may create fragmentation in malloc arenas. That's why, as I stated in my last letter, I was thinking about separating most frequently allocated static blocks into separate fixed-block allocator. But this is a long way to test if it is actually important and profitable - I just started messing with it :) Still my question is unanswered - according to your data, does or does not the system malloc have some optimization to deal with small allocs? If I allocate 1024 times 20 bytes (meaning 1024 zvals - pretty reasonable number for a decent application) - would I get 128K memory footprint instead of 20K memory footprint? Because if yes - we are in a dire need for more effective allocator.
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Sascha Schumann

23 years ago
(I don't have the time right now to write a lengthy reply) 1. malloc is expensive on some systems, and less expensive on many others. It really depends. 2. some mallocs (Linux, Solaris probably) are optimized for non-contentious allocations in threaded contexts (thread-specific memory arenas). 3. some mallocs (Linux) don't use binary fixed size chunks for small allocations (e.g. they won't allocate 128 bytes for a 80 byte chunk) 4. but some do (FreeBSD) 5. locking up memory in PHP and asserting that you basically would like to reroute malloc to go through PHP's emalloc does not sound reasonable to me. - Sascha

Stanislav Malyshev

23 years ago
SS>> 5. locking up memory in PHP and asserting that you basically SS>> would like to reroute malloc to go through PHP's emalloc SS>> does not sound reasonable to me. That's not even close to what I propose. I don't want mallocs to go thgough emalloc. Anyway, most of allocs in engine are emallocs anyway, and there is a very good reasons for that, outside of the discussion. I feel safe to suppose we are not going to discuss the need of emalloc and its memory cache here - I am not interested in this part anyway. What I wanted to check is if there may be improvement if _some_ allocations in _some_ parts of the engine would be done differently.
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Sterling Hughes

23 years ago
On Wed, 2003-06-18 at 09:21, Stanislav Malyshev wrote:
> SS>> Common referred to real applications, not 'make test'. > > Do you have some data about real applications in PHP that use strings > longer than 80 bytes via smart strings? My tests show most of the strings > used are small. Looking at the clients of this functions and taking into > account that prontf's are usually small and long printf are usually or > rewritten into heredocs (which don't use smart strings) or separated into > different functions. > BTW, why do you think "make test" is a bad test of memory allocation > patterns? What do you propose as a better test? >
Yep. cURL uses smart_str pretty extensively for reading data from the client. I have it alloc things in 4k chunks. -Sterling PS: I agree with your general pattern too. But I don't see where sascha's approach is limiting. I'll try reading again after coffee. :)
-- "The three most dangerous things in the world are a programmer with a soldering iron, a hardware type with a program patch and a user with an idea." - Unknown

Stanislav Malyshev

23 years ago
SH>> Yep. cURL uses smart_str pretty extensively for reading data from SH>> the client. I have it alloc things in 4k chunks. OK, point taken, though my patch does not influence cuRL in this case :)
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109

Stanislav Malyshev

23 years ago
SS>> If you move the additional check (newlen < SS>> SMART_STR_START_SIZE) and preallocation into the if (!(d)->c) SS>> branch, the changes won't affect the common code path. Ok, I have rewrote the patch, please see in attachment. It got somehow more inflated, but it now has no more than two conditionals in every codepath, as in original one.
-- Stanislav Malyshev, Zend Products Engineer stas@zend.com http://www.zend.com/ +972-3-6139665 ext.109