php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Request #57745 Reduce the memory penalty of using a CONSTANT_ARRAY
Submitted: 2007-07-13 10:44 UTC Modified: 2014-02-28 12:41 UTC
From: askalski at gmail dot com Assigned: krakjoe (profile)
Status: Closed Package: APC (PECL)
PHP Version: 5.2.1 OS: Linux / Apache
Private report: No CVE-ID: None
View Add Comment Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
You can add a comment by following this link or if you reported this bug, you can edit this bug over here.
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: askalski at gmail dot com
New email:
PHP Version: OS:

 

 [2007-07-13 10:44 UTC] askalski at gmail dot com
Description:
------------
Tested APC 3.0.14 with PHP 5.2.1 and PHP 4.4.7, as an apache2 (2.2.3) module.

Cached functions with parameters that default to array() are being copied from shared memory to the local process heap space upon retrieval from cache.

We are in the process of evaluating an upgrade from PHP4/mmcache to PHP5/APC, and our application is seeing a 40% increase in memory usage because of this issue.  (As a comparison, I built a modified version of APC that doesn't copy upon seeing a CONSTANT_ARRAY, and memory usage dropped back down to expected levels.)

Could this be implemented in a more memory-efficient manner?

Also, I would appreciate a small test case that demonstrates why CONSTANT_ARRAY opcodes can't be executed directly out of shared memory?  I tried several ways to break my modified no-copy APC, but couldn't force any aberrant behavior.

Thanks in advance.


APC settings in php.ini:

    [APC]
    apc.enabled = 1
    apc.shm_segments = 1
    apc.shm_size = 10
    apc.cache_by_default = On
    apc.mmap_file_mask = "/apc.shm.XXXXXX"


Reproduce code:
---------------
<?php

// function test($a = false)
function test($a = array())
{
    $b=1; $b=1; $b=1; $b=1; $b=1; $b=1; $b=1; $b=1;
    // repeat the previous line a bunch of times to
    // increase the function's memory footprint
}

echo memory_get_usage(), "\n";

?>


Expected result:
----------------
I expect function test($a = false) and function test($a = array()) to have similar memory_get_usage() numbers when served from cache (i.e. the second and subsequent page loads).


Actual result:
--------------
Only function test($a = false) shows a significant drop in memory usage.  The version with function test($a = array()) does not.


Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2007-07-13 11:11 UTC] gopalv82 at yahoo dot com
Race condition on the refcount of that array.

I had a bunch of SEGVs at high load, which is why the 
patch was inserted to check for these specific cases.

And the segvs stopped ... do you want me to take a second look ? :)
 [2007-07-13 13:46 UTC] askalski at gmail dot com
Thank you for your prompt reply.

Knowing that it's refcount related allowed me to reproduce the SEGV with my modified (needcopy=0) version of APC.  Here's the test script I ran many times concurrently:

<?php

function x($a = array(1, 2, 3, 4, 5))
{
}

for ($i = 0; $i < 100000; $i++)
{
    x();
}

?>

This crashed PHP 4.4.7, but not PHP 5.2.1.  (It also turns out that mmcache crashes on this - heh.)

This leaves two questions in my mind:

- Is it still an issue as of the most recent versions of PHP5?
- Is there a workaround that doesn't require copying the entire function into local process space?
 [2007-07-13 15:12 UTC] gopalv82 at yahoo dot com
As for it being fixed in php-5.x, I have completely forgotten
*where* the race condition was (*heh*) to reconfirm it is 
fixed now (somewhere in RECV_INIT opcode). I lost my 
valgrind patches when my main OS disk crashed, which is 
what I was using to track race conditions. They will 
have to be re-synthesised for the latest valgrind
before I can debug this.

The workaround typically involves fixing Zend if it still 
clobbers refcount in those arrays. Sort of one leading to 
the other.
 [2007-07-14 19:19 UTC] askalski at gmail dot com
I managed to crash PHP 5.2.3 with my non-copying APC.  The reason it didn't crash before was a matter of hardware.  My 4.4.7 test box was a dual-cpu hyperthreading xeon, and the 5.2.1 box was a single athlon cpu.  When I built and tested 5.2.3 on the dual-cpu box, the script crashed readily.

I took a closer look at what's going on.  As you mentioned already, it looks like a refcount race condition.  Because RECV_INIT only does a shallow copy of a CONSTANT_ARRAY parameter, all of the member zvals are still in shared memory.  The shallow copy (zval_copy_ctor) increments (++) the member zval refcounts.

Later, after the function returns, the caller does a zend_hash_clean on the old symbol table.  This frees the shallow-copied array, and decrements (--) the refcounts of the member zvals (still in shared memory).

Because the C ++ and -- operators are non-atomic (load, modify, store), and the Zend engine does no mutex locking of refcounts, _zval_ptr_dtor will eventually attempt to free the zval in shared memory.

I verified this with two different quick & dirty fixes.  The first fix was to use a SYSV semaphore to prevent RECV_INIT's zval_copy_ctor from running concurrently with the zend_hash_clean on the symbol table.  Although execution was slowed by an order of magnitude, the SEGV went away.  An alternate fix I tried involved adding an "immortal" flag to the zval struct.  I had APC set the flag on all CONSTANT_ARRAY members, and changed _zval_ptr_dtor to do nothing when the "immortal" flag is set.  This also made the SEGV go away.

Because the race condition is on the refcounts of the array members, the most obvious improvement would be to deep-copy only for non-empty arrays.  In my application, the empty array makes up the overwhelming majority of default-parameter cases, so this simple patch would be enough to satisfy my needs.
 [2007-08-23 09:25 UTC] askalski at gmail dot com
Since I haven't heard anything about this in over a month, I'll paste the patch I've been using to work around the memory issue.  The idea here is that an empty CONSTANT_ARRAY shouldn't force a deep copy, since the race condition is on array members (empty array = no members = no crash).

--- APC-3.0.14-orig/apc_compile.c       2007-04-02 19:05:30.000000000 -0400
+++ APC-3.0.14/apc_compile.c    2007-07-16 12:22:41.000000000 -0400
@@ -1235,7 +1235,8 @@
 #endif
             case ZEND_RECV_INIT:
                 if(zo->op2.op_type == IS_CONST &&
-                    zo->op2.u.constant.type == IS_CONSTANT_ARRAY) {
+                    zo->op2.u.constant.type == IS_CONSTANT_ARRAY &&
+                    zo->op2.u.constant.value.ht->nNumOfElements) {
                     if(flags != NULL) {
                         flags->deep_copy = 1;
                     }
@@ -1243,9 +1244,11 @@
                 break;
             default:
                 if((zo->op1.op_type == IS_CONST &&
-                    zo->op1.u.constant.type == IS_CONSTANT_ARRAY) ||
+                    zo->op1.u.constant.type == IS_CONSTANT_ARRAY &&
+                    zo->op1.u.constant.value.ht->nNumOfElements) ||
                     (zo->op2.op_type == IS_CONST &&
-                        zo->op2.u.constant.type == IS_CONSTANT_ARRAY)) {
+                        zo->op2.u.constant.type == IS_CONSTANT_ARRAY &&
+                        zo->op2.u.constant.value.ht->nNumOfElements)) {
                     if(flags != NULL) {
                         flags->deep_copy = 1;
                     }
@@ -2010,9 +2013,11 @@
         zo = &src->opcodes[j];

         if( ((zo->op1.op_type == IS_CONST &&
-              zo->op1.u.constant.type == IS_CONSTANT_ARRAY)) ||
+              zo->op1.u.constant.type == IS_CONSTANT_ARRAY) &&
+              zo->op1.u.constant.value.ht->nNumOfElements) ||
             ((zo->op2.op_type == IS_CONST &&
-              zo->op2.u.constant.type == IS_CONSTANT_ARRAY))) {
+              zo->op2.u.constant.type == IS_CONSTANT_ARRAY) &&
+              zo->op2.u.constant.value.ht->nNumOfElements)) {
             needcopy = 1;
         }
     }
 [2008-01-29 21:48 UTC] shire@php.net
As a follow up to this, I'm currently beginning tests of this in our production environment.  I'll try to follow up with any problems I find in the next few weeks. (it seems to be working fine for now).
 [2008-08-19 19:38 UTC] shire@php.net
This has been working pretty consistently for me in production the last few months, probably worth investigating more at some point to see if we can replicate this bug.
 [2014-02-28 12:41 UTC] krakjoe@php.net
-Status: Open +Status: Closed -Assigned To: +Assigned To: krakjoe
 [2014-02-28 12:41 UTC] krakjoe@php.net
Closing this bug for several reasons:

 5.2 is unsupported software.
 APC is no longer the primary means of caching opcodes.

Thanks for helping to make PHP better :)
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Thu Apr 25 16:01:28 2024 UTC