[Bench] Problème avec memory_get_usage()

Répondre


Cette question est un moyen d’empêcher des soumissions automatisées de formulaires par des robots.
Smileys
:D :) :( :o :shock: :? 8-) :lol: :x :P :oops: :cry: :evil: :twisted: :roll: :wink: :!: :?: :idea: :arrow: :| :mrgreen: =D> #-o =P~ :^o :non: :priere: 8-|
Voir plus de smileys
  Revue du sujet
 

  Étendre la vue Revue du sujet : [Bench] Problème avec memory_get_usage()

par titerm » 24 févr. 2008, 11:28

Concernant le GC, il n'est pas forcement appelé à tout bout de champ car il a un coût qui ne sera pas forcement justifié sur la durée d'un page.

Par exemple, pour APC, il n'est appelé que lorsqu'il y a un "défault page" (un pb d'allocation). Quand on essaie de stocker une variable dans la zone utilisateur, c'est seulement si il y a un pb d'allocation que le GC est appelé, sinon, il va remplir le max de la zone allouée, et ce, meme si le TTL des variables est dépassée.

Dans le cas de php, le GC ne s'active probablement qu'au delà d'un certain seuil ou d'une certaine action afin de ne pas générer un surcoût inutile puisque la durée de vie d'un script php est relativement limité dans le temps.

par Sékiltoyai » 23 févr. 2008, 22:52

Bon, alors le sujet n'est pas loin d'être résolu alors ?

Mais bon, ca me sidère la façon dont php est fait…

par Hubert Roksor » 23 févr. 2008, 21:34

Je savais déjà pour l'invocation directe des méthodes, mais je n'avais pas essayé de faire plusieurs boucles en utilisant l'accès magique, et c'est là que le mystère s'éclaircit : il s'agit certainement d'une lookup table qui se remplit au fur et à mesure qu'on accède à des propriétés différentes.

Ça expliquerait pourquoi des invocations successives ne modifient pas la consommation mémoire, et pourquoi détruire l'objet avec unset() libère de la mémoire. D'ailleurs, on peut vérifier que cette table est partagée par toutes les méthodes magiques.

En revanche, ça n'explique pas pourquoi chaque entrée nécessite pratiquement 80 octets de mémoire et pourquoi elle ne se purge pas automatiquement. Les devs PHP ont probablement pensé que personne n'utiliserait autant de variables différentes, j'imagine. Et c'est vrai qu'il est rare d'utiliser des milliers de variables différentes, donc j'arrive à comprendre que ce ne soit pas une priorité (moins d'un mégaoctet pour 10 000, ce n'est pas vraiment énorme au fond). À noter que pour forcer cette table à se vider, il est possible d'utiliser
$obj = clone $obj;
(pas sûr des effets collatéraux néanmoins)

par Sékiltoyai » 23 févr. 2008, 20:55

Relativement à l'éventuelle fuite mémoire dans les fonctions magiques. J'ai fait un test. Au départ je voulais voir si le problème était inhérent à toute fonction, et qu'en définitive, ce soient simplement les arguments de la fonction qui n'ont pas été libérés.
Et par hasard, je suis arrivé à un test donnant un résultat quelque peu … étrange :

Code : Tout sélectionner

0k // Utilisation de $obj->get($var) au cas où le problème soit un peu plus global voire même attendu 0k // Test sur $obj->__get($var) pour tester l'appel comme une fonction normale 1380k // Utilisation de $obj->var 0k // Seconde utilisation de $obj->var
<?php

class obj 
{ 
    public function __get($k) { unset($k); } 
    public function get($k) {} 
} 

$i = $i2 = $i3 = $i4 = 10000; 
$obj = new obj; 

$me = memory_get_usage(); 
do 
{
    $obj->get('var' . $i);
} 
while (--$i); 

//unset($obj); 
echo (memory_get_usage() - $me) >> 10, 'k<br />';

$me = memory_get_usage(); 
do 
{
    $obj->__get('var' . $i2); 
} 
while (--$i2); 

//unset($obj); 
echo (memory_get_usage() - $me) >> 10, 'k<br />';

$me = memory_get_usage(); 
do 
{
    $obj->{'var' . $i3}; 
} 
while (--$i3); 

//unset($obj); 
echo (memory_get_usage() - $me) >> 10, 'k<br />';

$me = memory_get_usage(); 
do 
{
    $obj->{'var' . $i4}; 
} 
while (--$i4); 

//unset($obj); 
echo (memory_get_usage() - $me) >> 10, 'k<br />';

?>

par Hubert Roksor » 23 févr. 2008, 13:37

le développeur sait qu'elles ont étés allouées, il serait beaucoup plus logique qu'elles soient libérées en dur aussi…
C'est probablement plus efficace de le faire en lot ?
Je parlais pour le stockage de 1000 valeurs dans un tableau tout simple (sans passage par un objet), qui prend 160k dans php, alors que ca n'en nécessaiterait théoriquement pas plus de 16k pour stocker les noms des clés et les valeurs
Joker sur celle-ci. Il y a certains arrondis (multiple d'une puissance de 2, probablement 8) à chaque fois que PHP réserve de la mémoire pour une chaîne, plus quelques octets pour chaque variable (= membre d'un tableau) pour stocker certains bits (is_empty, is_ref et autres) ainsi qu'un mot 16bit pour le compteur de références et probablement d'autres trucs auxquels je ne pense pas tout de suite, probablement un mot 32bit pour stocker la longueur de la chaîne qui représente l'index. À celà tu rajoutes l'overhead pour gérer notre cher tableau-liste-pile et tu dois arriver à ~300% d'overhead + arrondis.

Si tu veux te faire une idée de l'overhead,
$i = $x = 1000;
$arr = array();

$me = memory_get_usage();
do
{
	$arr[md5($i)] = $x;
}
while (--$i);
echo (memory_get_usage() - $me) >> 10, 'k :: ', debug_zval_dump($x);
1000 éléments ayant un index sur 32 octets = 82 KiB utilisés. J'utilise spécifiquement une variable pour l'assignation, pour tirer parti du système de refcount et utiliser le moins de mémoire possible.

par Sékiltoyai » 23 févr. 2008, 13:13

]Par quoi est ce que tu entends que php aurait besoin de variables supplémentaires qu'il n'aurait pas libéré lui même ?
À ma connaissance, PHP a souvent besoin de variables temporaires pour des tas de raisons. Si mes souvenirs sont bons, $i++ nécessite une variable temporaire pour stocker la valeur de $i avant son incrémentation. Pareil pour quelque chose comme $a = $a . $b, et c'est pour cela que $a .= $b est plus rapide. Évidemment, tout cela reste à vérifier, ne me citez pas :P
Oui, donc c'est bien ce que je pensais aussi. :-/
Une fois l'exécution de PHP terminé oui, mais pas en milieu de script. Les variables internes dont je parlais sont les fameuses "zval", mais j'ai bien peur que mes connaissances du moteur de PHP ne me permettent pas d'être plus précis. Je sais que le moteur utilise des zval un peu partout, et que le GC se déclenche de temps à autres pour libérer celles qui ne sont plus utilisées quand il y en a trop qui traînent. Pour les détails, il faudra creuser la mailing list internals.
Je pensais après l'opération en elle même, c'est à dire que pour le cas d'une concaténation par exemple, les variables qui en interne ont servi à concaténer devraient être déallouées juste après la concaténation, et non pas attendre le GC, vu que ce sont des variables qui sont complètement contrôlées par le moteur, elles sont créées dans le code du moteur, le développeur sait qu'elles ont étés allouées, il serait beaucoup plus logique qu'elles soient libérées en dur aussi…

Et ca n'explique pas un facteur 10 entre la consommation de mémoire des données brutes et l'utilisation mémoire de php. Tu n'as pas d'explication pour ca ?
Je ne le vois pas en pourcentage. D'après mes tests, la différence de mémoire correspond exactement à la mémoire "perdue" dans __set() et dépend de la taille du nom de la variable.
Je parlais pour le stockage de 1000 valeurs dans un tableau tout simple (sans passage par un objet), qui prend 160k dans php, alors que ca n'en nécessaiterait théoriquement pas plus de 16k pour stocker les noms des clés et les valeurs. (Mais si mon calcul est caduque, dis le moi).

par Hubert Roksor » 23 févr. 2008, 12:49

si jamais tu as des variables utilisées en interne pour gérer les opérations d'affectation, de tableau, …, elle doivent normalement être libérées par le noyau une fois l'opération terminée…
Une fois l'exécution de PHP terminé oui, mais pas en milieu de script. Les variables internes dont je parlais sont les fameuses "zval", mais j'ai bien peur que mes connaissances du moteur de PHP ne me permettent pas d'être plus précis. Je sais que le moteur utilise des zval un peu partout, et que le GC se déclenche de temps à autres pour libérer celles qui ne sont plus utilisées quand il y en a trop qui traînent. Pour les détails, il faudra creuser la mailing list internals.
Et ca n'explique pas un facteur 10 entre la consommation de mémoire des données brutes et l'utilisation mémoire de php. Tu n'as pas d'explication pour ca ?
Je ne le vois pas en pourcentage. D'après mes tests, la différence de mémoire correspond exactement à la mémoire "perdue" dans __set() et dépend de la taille du nom de la variable.
]Par quoi est ce que tu entends que php aurait besoin de variables supplémentaires qu'il n'aurait pas libéré lui même ?
À ma connaissance, PHP a souvent besoin de variables temporaires pour des tas de raisons. Si mes souvenirs sont bons, $i++ nécessite une variable temporaire pour stocker la valeur de $i avant son incrémentation. Pareil pour quelque chose comme $a = $a . $b, et c'est pour cela que $a .= $b est plus rapide. Évidemment, tout cela reste à vérifier, ne me citez pas :P

par Sékiltoyai » 23 févr. 2008, 12:26

La différence de 10x entre les deux scripts ne... euh... m'étonne pas trop puisqu'on utilise des valeurs de $i différentes. :roll:
Merde, c'est 10000, le con :D . Alors au temps pour moi :)
Quand je parlais de variables temporaires, je pensais aussi aux trucs utilisés en internes, même si le ZE est sensé réutiliser leur mémoire si mes souvenirs sont bons.
Si c'est utilisé en interne, la libération des ressources devraient être faites directement dans le code et ne pas attendre le GC. C'est à dire que si jamais tu as des variables utilisées en interne pour gérer les opérations d'affectation, de tableau, …, elle doivent normalement être libérées par le noyau une fois l'opération terminée…
Et ca n'explique pas un facteur 10 entre la consommation de mémoire des données brutes et l'utilisation mémoire de php. Tu n'as pas d'explication pour ca ? Par quoi est ce que tu entends que php aurait besoin de variables supplémentaires qu'il n'aurait pas libéré lui même ?
Après vérification, je confirme que la mémoire est perdue dans __set(), qui ne rend pas la mémoire allouée pour $k. On peut le reproduire avec n'importe quelle fonction magique. Tiens-moi au courant si tu rapportes ce bug (après avoir vérifié s'il avait déjà été débattu) et sinon, je le ferai moi-même.
Je regarderais une fois la discussion finie.

par Hubert Roksor » 23 févr. 2008, 07:05

Quand je parlais de variables temporaires, je pensais aussi aux trucs utilisés en internes, même si le ZE est sensé réutiliser leur mémoire si mes souvenirs sont bons.

La différence de 10x entre les deux scripts ne... euh... m'étonne pas trop puisqu'on utilise des valeurs de $i différentes. :roll:

Après vérification, je confirme que la mémoire est perdue dans __set(), qui ne rend pas la mémoire allouée pour $k. On peut le reproduire avec n'importe quelle fonction magique. Tiens-moi au courant si tu rapportes ce bug (après avoir vérifié s'il avait déjà été débattu) et sinon, je le ferai moi-même.
class obj
{
	public function __get($k) {}
}

$i = 10000;
$obj = new obj;

$me = memory_get_usage();
do
{
	$k = 'var' . $i;
	$obj->$k;
}
while (--$i);

//unset($obj);
echo (memory_get_usage() - $me) >> 10, 'k';
Le script utilise 767k sur ma machine (dépend de la taille de $k) mais la consommation tombe à 62k si je unset($obj) avant de remesurer.

Testé sur
  • WinXP - PHP 5.2.6-dev (cli) (built: Feb 23 2008 00:05:23)
  • WinXP - PHP 5.3.0-dev (cli) (built: Feb 20 2008 08:17:59)

par Sékiltoyai » 23 févr. 2008, 01:43

Ce qui est marrant, c'est que selon la clé par laquelle on commence le remplissage de la table de hashage, on aura des résultats d'un facteur 10…
Avec le script de Hubert, on a du 1500k, avec celui ci on a du 150k :
<?php

$obj = new obj(); 

$me = memory_get_usage(); 
for($i=0; $i<1000; $i++)
{ 
//    $obj->{'var' . $i} = 0; 
    $obj->arr['var' . $i] = 0; 
}

//$obj = unserialize(serialize($obj)); 

echo (memory_get_usage() - $me) >> 10, 'k :: CRC ', crc32(serialize($obj)); 

class obj 
{ 
    public $arr = array(); 
    public function __set($k, $v) 
    { 
        $this->arr[$k] = $v; 
    } 
}

?>
Après, je dis ca, je dis rien…

par Sékiltoyai » 23 févr. 2008, 01:36

Regarde le script de Hubert. Il est simplissime, à fortiori lorsque l'on utilise la syntaxe "$obj->arr['var' . $i]" il n'y a aucune erreur de code qui pourrait plomber les performances.
Sur mon script, tu peux peut être dire qu'il n'est pas forcément optimal puisqu'il utilise pas mal de variables, et une classe dont tu ne connais pas le contenu, mais celui de Hubert, je te demande en quoi il serait imparfait.
Voilà, comme je le disais, c'est du codage de base, à moins que tu n'y trouves une erreur significative ou bien un test concluant, si sur un script tel que celui de Hubert donne de tels résultats, le problème est bel et bien au niveau du noyau, et il n'est pas des moindres… Et quand je disais fuite mémoire, j'incluais un mécanisme comparable qu'il pourrait y avoir avec la fonction emalloc du noyau php.

par Genova » 23 févr. 2008, 01:26

A mon avis c'est pas PHP qui cloche, mais ton test. Ca parait tellement énorme comme "fuite de mémoire" (pas vraiment le bon terme, puisque les fuites de mémoires sont dues à des zones mémoires non libérées avant réalocation au fil du script). Je pense que les dev PHP sont un minimum au courant de la mémoire utilisée, puisqu'ils ont optimisées leur moteur au fil des versions (et qui dit optimisation et réécriture du moteur, dit qu'on se soucie en priorité des questions de mémoires, surtout quand il s'agit d'un interpréteur de langage).

par Sékiltoyai » 23 févr. 2008, 01:20

J'ai retesté un peu et c'est plus incompréhensible encore.
Déjà le test que tu m'as donné Hubert donne des résultats 10 fois plus lourds que le mien, c'est incompréhensible quand on sait que ce script est moins lourd et qu'il assigne une quantité de données plus faible (4 octets, 8 au maximum pour un entier contre les 7,5 octets en moyenne des chaines).

Ensuite, titerm, bien entendu php a besoin de place supplémentaire pour gérer les données, c'est inhérent à tout langage interprété, il faut stocker avec la variable des meta-informations comme son type, pour php le nombre de références, ou encore sa portée. Mais toutes ces informations sont numériques (entiers ou adresses) et, dans mon cas, au mieux 160K de méta-données pour 16K de données réelles (en comptant le nom de la variable), ca me paraît vraiment énorme.

Après, dire que le garbage collector n'a pas fait son boulot, c'est possible, mais si on prend ton test Hubert, tu n'utilises aucune variable supplémentaire et on ne peut pas prétexter une non destruction des variables non utilisées.

Bref, pour l'instant je n'ai pas été convaincu qu'il n'y a pas un très grave problème d'optimisation au niveau du noyau php. En l'occurence une table de hashage (et encore, si c'est la structure de donnée qui est utilisée) qui prend 10 à 100 fois la taille qu'elle devrait prendre, ca me choque, et autant je ne me demande plus si php mesure mal la mémoire, autant, maintenant, je me pose la question du problème de mémoire, voire même des fuites mémoires au niveau du noyau ou bien en aval dans les fonctions utilisant emalloc.

par Hubert Roksor » 21 févr. 2008, 19:20

Après vérification, il semblerait que le problème vienne de __set() et/ou un mécanisme associé. Version courte du test :
$i = 10000;
$obj = new obj;

$me = memory_get_usage();
do
{
	$obj->{'var' . $i} = 0;
//	$obj->arr['var' . $i] = 0;
}
while (--$i);

//$obj = unserialize(serialize($obj));

echo (memory_get_usage() - $me) >> 10, 'k :: CRC ', crc32(serialize($obj));

class obj
{
	public $arr = array();
	public function __set($k, $v)
	{
		$this->arr[$k] = $v;
	}
}
On voit que selon la méthode utilisée, la consommation mémoire diffère même si le résultat est le même (le CRC est identique). En y réfléchissant, je me rappelle avoir lu un truc il y a quelques mois de cela sur la mailing list de PHP, au sujet de quelqu'un qui utilisait unserialize(serialize($obj)) pour faire baisser sa consommation mémoire. Après vérification, effectivement dans le cas présent, ça marche... des fois. J'ai

Code : Tout sélectionner

845k pour $obj->arr['var' . $i] 1613k pour $obj->{'var' . $i} 908k pour $obj->arr['var' . $i] suivi de unserialize(serialize()) 908k pour $obj->{'var' . $i} suivi de unserialize(serialize())
Et en creusant encore un peu, on peut en déduire que le problème vient de la façon dont le tableau est peuplé en interne, puisque l'on peut remplacer le un/serialize par
$obj->arr = array_combine(array_keys($obj->arr), array_values($obj->arr));
...pour obtenir le même résultat.

par titerm » 21 févr. 2008, 19:13

Je te suggère de lire cet article très instructif sur la facon dont php gère les variables.

http://www.derickrethans.nl/files/phpar ... rticle.pdf

php gère une structure interne pour chaque variable (variable contener) qui vas bien au dela du nom et du contenu de la variable.