Camlight et malloc

Je reproduis ici mon dernier message sur la DUF:
camllight utilise une pile dynamiquement étendue vers le bas via des mallocs
judicieux. Or dans la version 64 bits, cette pile est soudainement saturée
très rapidement (trop). Qui plus est un free propre de la dernière allocation
plante le système. En fouillant, je me suis aperçu que la première allocation
n’est pas contigue des suivantes. Pour être exact voilà ce que donne la
succession d’appels de la fonction

char *xmallocverbeux(asize_t size) {
char *p;
printf("->demande de %d\n",size);
p=xmalloc(size);
printf("<-0x%16x “,p);
xfree§;
p=xmalloc(size);
printf(”<<-0x%16x\n",p);
return§;
}
(xmalloc étant malloc):

<-0x 5a768010 <<-0x 1bb3820
<-0x 1c17a40 <<-0x 1c17a40
<-0x 1c58aa0 <<-0x 1c58aa0
<-0x 1d50af0 <<-0x 1d50af0
<-0x 1d91b10 <<-0x 1d91b10
<-0x 1dd2b30 <<-0x 1dd2b30

La première ligne est celle qui met le bazar, en effet sans l’appel
malloc-free-malloc (illogique) la première allocation définissnant le sommet de
la pile puis les autres étant des augmentations successives, la pile ne peut
être étendu et bing «Out of memory» (ce qui énerve assez avec 4G de RAM)

Il semble que malloc n’assure pas la continuité des blocs mémoire, realloc le ferait mais au prix d’un déplacement éventuel de la mémoire et surtout sans préserver la fin de la mémoire. Visiblement camllight fait grossir sa pile dans vers le bas, quelqu’un aurait-il une solution (sans toucher le code de malloc)?

PS: En attendant j’ai mis sur mon dépot une nouvelle version avec une rustine temporaire.

Espérer que deux allocations successives seront contiguës est une chimère : malloc ne fournit aucune garantie quant à l’emplacement des zones allouées (sans même parler des détails d’implémentation comme les zones sentinelles et autres joyeusetés). D’après ton explication on nage en plein undefined behaviour (ce qui est la pire situation en programmation, à mon avis), et j’ai peur qu’il n’y ait pas de solution en C “simple” si on veut respecter les standards. À vrai dire c’est même du pur hasard que ça marche correctement sur i386. :108
Cela dit, peut-être que certaines libc ou certaines API du kernel donnent des garanties supplémentaires (j’en sais rien) mais du coup adieu la portabilité !

Sans connaître le fonctionnement interne de camllight et en me basant uniquement sur ce que tu en as expliqué, j’aurais tendance à dire qu’il y a là une énorme erreur de design. :confused:
Mais c’est tellement gros que j’ai peur de ne pas avoir tous les éléments pour bien comprendre le problème, c’est pas possible autrement… :open_mouth:

Il y a deux spbms, ce pbm de conception, il faut que je rentre plus dans le code mais en tout cas, le fait est que si le pointeur retourné n’est pas comme il faut il dégage en «mémoire insuffisante» et puis cette rustine bizarre qui semble fonctionner…

Je viens de lire la discussion sur d-u-f, ça me rassure de voir que je ne suis pas le seul à penser ça… Il y a bien un gros problème dans camllight. :confused:

Par contre je viens de percuter : tu dis que la pile s’étend vers le bas (adresses décroissantes donc) et que le haut de la pile ne bouge pas, mais ça semble contredit par le fait que la trace que tu fournis montre des allocations en adresses croissantes. :confusion-questionmarks:

Question : est-ce que camllight fait appel à des libs tierces susceptibles d’allouer de la mémoire, permet de faire des dlopen et autres trucs du genre ? Si ça n’est pas le cas (ce que je pense, sinon même sur i386 tu aurais des sacrés soucis car impossible d’allouer des blocs contigus après qu’une lib tierce ait fait un malloc), tu peux peut-être essayer de remplacer tes mallocs par brk/sbrk (APIs kernel pour étendre la mémoire du process) ?
Ça court-circuiterait totalement la gestion de la mémoire par malloc et ça rendrait le code dépendant des kernels Linux/BSD, mais ça garantirait la contiguïté des zones avec des allocations croissantes (comme le montre ta trace). Par contre tu peux dire adieu à malloc (et donc aux libs tierces), ce n’est pas du tout compatible.

Une autre possibilité, mais qui est aussi très dépendante du kernel et de sa configuration : profiter du fait que par défaut Linux alloue la mémoire de manière optimiste (il autorise l’overcommit) et allouer directement un énorme bloc de mémoire, genre équivalent à la totalité de la RAM. Après ça tu n’auras plus besoin d’aucune allocation supplémentaire, et seules les pages modifiées seront réellement allouées. De mémoire SBCL a un comportement de ce genre.
Avantage : c’est tout à fait compatible avec des libs tierces qui allouent de la mémoire dans ton dos.
Gros inconvénient par contre : un admin peut configurer le kernel pour désactiver l’allocation optimiste, et ça fout toute cette stratégie en l’air (man malloc : For more information, see the description of /proc/sys/vm/overcommit_memory and /proc/sys/vm/oom_adj in proc(5), and the kernel source file Documentation/vm/overcommit-accounting).
Autre inconvénient mineur : ça n’est viable que sur amd64 où l’espace d’adressage est beaucoup plus grand que la quantité de RAM disponible. Sur i386, allouer 2Go (ou 3Go, je sais plus où se situe la frontière user/kernel sur Linux) ça te bouffe tout ton espace d’adressage utilisateur et il ne reste plus aucune place pour les allocations de libs tierces.

Ces deux méthodes ne sont pas trop invasives je pense, les modifs à faire devraient être assez localisées.

Les joies de l’UB… :confused: Malheureusement le fait que ça semble fonctionner ici et maintenant ne te fournit aucune garantie pour l’avenir puisque le problème de fond n’est pas réglé, au moindre changement de la stratégie d’allocation de la libc (voire même simplement en changeant de libc) ça peut réapparaître.

Je viens de lire les remarques de Syam. N’est-ce pas un peu dangereux de se lier au noyau de la sorte ? Que cela soit par des appels directs au noyau ou l’utilisation en toute confiance de l’overcommit. J’ai de très mauvais souvenirs de telles méthodes lors du passage du 2.6.24 au 2.6.25. Cela n’avait pas de lien direct avec le problème ici évoqué mais quand on doit passer du temps pour se maintenir une branche du noyau ça marque.

Ceci dit j’allais répondre sur la duf mais tant que je suis là. J’ai connu ce genre de problème d’allocations/libérations foireuses sous micro-contrôleur. A l’époque la seule solution viable avait été de s’allouer des plages importantes de mémoire et de bricoler un allocateur/réallocateur par dessus. On travaillait au chausse-pied dans des chaussons taille 22 mais on pouvait se payer le luxe d’avoir des piles de données continues réallouables.
Bon ok je propose de réinventer la roue et en plus je n’ose à peine imaginer l’influence que cela peut avoir sur le code de camlight, que je ne connais ni d’Eve ni d’Adam.

D’ailleurs qu’en disent les dev de camlight ?

Oui effectivement, j’ignore pourquoi j’ai dit de bas en haut (j’avais dans ma tête le schéma standard). Je vais essayer avec un realloc du coup. Quant à allouer un gros morceau d’un coup, quitte à modifier les choses, autant le faire proprement.

Pour DjKlaus: camllight dort depuis pas mal de temps, en fait je crois que seul moi s’occupe encore du code. J’ai envoyé un message aux développeurs initiaux pour savoir si ils ont des notes sur le cod; il y a très peu de commentaires (voire aucun)

Là on parle quand même d’interfaces et de comportements du noyau qui sont extrêmement bien spécifiés, et qui n’ont aucune chance de changer (je ne sais pas ce qui t’a posé problème dans le passé mais je soupçonne que c’était des interfaces expérimentales).
Après, évidemment ces deux méthodes dont je parlais ont chacune leurs inconvénients distincts (sbrk ne cohabite pas avec malloc, l’overcommit est désactivable par l’admin) donc elles sont loin d’être idéales, mais c’est pas évident de trouver une solution viable pour garantir des allocations contiguës (obligatoire vu qu’on parle d’une pile, si le code de camllight pouvait être modifié pour utiliser un modèle “tas” la question ne se poserait pas mais ça impliquerait certainement un remaniement énorme de l’ensemble).

Oui ça semble correct, à condition que tu n’aies jamais de pointeurs absolus dans la pile (car realloc peut bouger la zone mémoire : au niveau du standard il n’y a pas non plus de garantie que la zone soit simplement étendue sans changer d’emplacement, même si c’est possible – encore une fois, espérer conserver une adresse de départ immuable après un realloc c’est foncer droit dans de l’UB).

[quote=“syam”]

Oui ça semble correct, à condition que tu n’aies jamais de pointeurs absolus dans la pile (car realloc peut bouger la zone mémoire : au niveau du standard il n’y a pas non plus de garantie que la zone soit simplement étendue sans changer d’emplacement, même si c’est possible – encore une fois, espérer conserver une adresse de départ immuable après un realloc c’est foncer droit dans de l’UB).[/quote]

Ben oui, je verrais ça au cas par cas, pour le moment il me faut récupérer le pointeur. Le code fait l’alignement lui même donc le pointeur de départ est perdu… Il faut soit que je le récupère soit que je supprime tout alignement en utilisant celui que malloc a sans doute. Je pense que ce code n’est pas tout à fait parfait!!! À suivre… Je vais me corriger une ou deux copies, ça va me changer un peu (ce qui n’est pas peu dire! Jamais je n’aurais pensé écrire une telle phrase un jour)

OK. J’ai sorti le parachute parce qu’il est toujours bon de se poser des questions quand on commence à parler interfaces noyau. Je dois aussi avouer que j’ai eu la flemme d’aller me documenter sur ces appels.

Pire, du propriétaire.

Bon, en plus il n’y a pas de realloc avec alignement… Sale journée.

mmap peut-être ? Tu es forcément aligné sur une page (minimum 4k), tu peux fournir une adresse comme “conseil” (adresse contiguë au dernier mmap effectué, donc) et tu peux facilement détecter si la zone allouée est bien celle que tu voulais (conseil == obtenu ?), sinon tu sors proprement (OOM). Apparemment ça cohabite bien avec malloc, et c’est beaucoup plus propre que (s)brk même si ça reste un appel noyau.

Rooh il doit bien y avoir une solution sans avoir à retoucher à tout le code quand même… :mrgreen:

Je viens de me taper un realloc en tablant sur un alignement (vrai en pratique) juste pour voir si tout est relatif par rapport à heap_start et heap_end. L’amusant est que cette fois, heap_start descend réellement alors que jusqu’à présent ça augmentait par le haut. J’ai eu droit à ça…

[code]allocation de 0x40000 octets vers 0X9431A010
allocation de 0x8800 octets vers 0X1B4C010
Let s make heap man!
allocation alignée de 0x40010 octets vers 0X942D9010
Heap_start initalisé à 0X942D9010
hum
humhum
test ecriture sur 0X942D9020
ecriture
ecriture2
ayé
allocation de 0x100 octets vers 0X1B54820
allocation de 0x4000 octets vers 0X1B54930
allocation de 0x4000 octets vers 0X1B58940
allocation de 0x4000 octets vers 0X1B5C950
allocation de 0x13f66 octets vers 0X1B60960
allocation de 0x1028 octets vers 0X1B748D0
Hum sur 0X94309768
Libération de 0X1B748D0
allocation de 0x1028 octets vers 0X1B748D0
allocation de 0x1028 octets vers 0X1B75900
allocation de 0x1028 octets vers 0X1B76930

  Caml Light version 0.81-2

allocation de 0x1028 octets vers 0X1B77960
Hum sur 0X94300418
Hum sur 0X942FFB98
Hum sur 0X942FF458
Libération de 0X1B77960
allocation de 0x1028 octets vers 0X1B77960
Hum sur 0X942FE0C8
Libération de 0X1B77960
[…]
Hum sur 0X942E0210
#allocation de 0x23 octets vers 0X1B77960
Hum sur 0X942DCA08
puissance : int -> int -> int =
#allocation de 0x53 octets vers 0X1B77990
puissance : int -> int -> int =
#allocation de 0x51 octets vers 0X1B779F0
0X942D9020 -> 0X94319020
Avant: 0X942D9020 -> 0X94319020
Apres: 0X94297020 -> 0X942D7020
Test OK
Test2 OK
On rend 0X942D7038
Hum sur 0X94313028
Segmentation fault
[/code]
Le avant après est le changement heap_start heap_end, les hum et humhum sont les écritures qui font des segfaults. Le segfault a lieu lors de l’écriture sur une zone après le heap_end. Je vais voir si je ne me suis pas gourré dans le calcul de la taille… Je me décide à me replonger dans la syntaxe de gdb, les printf vont être un chouïa insuffisant.