OVH Cloud OVH Cloud

Allouer plus de mémoire que disponible

76 réponses
Avatar
Rémi Moyen
Salut,

J'ai une question sur la mani=E8re dont se comporte un programme sous
Linux, en C++, quand on essaye d'allouer plus de m=E9moire que
disponible sur le syst=E8me. =C0 la base, l'op=E9rateur new lance dans ce
cas-l=E0 une exception et c'est =E0 moi de la rattraper et de la traiter,
tr=E8s bien.

Mais mon probl=E8me est de savoir si, par derri=E8re, le noyau essaye
d'allouer toute la m=E9moire avant de se rendre compte qu'il n'en a pas
assez de disponible, ou est-ce qu'il fait d'abord une v=E9rification
avant de se lancer l=E0-dedans ?

Dans le cas o=F9 la m=E9moire est sur de la RAM, =E7a ne change probablement
pas grand chose (en perception par l'utilisateur), mais si il y a du
swap, est-ce que le noyau va faire swapper tout ce qu'il peut pendant
une heure avant de se rendre compte qu'il lui manque 42 octets ?

Si c'est le cas, alors j'imagine que j'ai int=E9r=EAt =E0 impl=E9menter
quelques checks rapides sur la m=E9moire disponible avant de commencer =E0
allouer des blocs de 1 Go (=E7a m'arrive...), sinon mes utilisateurs
risquent de ne pas trop aimer devoir attendre longtemps, et swapper
tout les autres programmes, avant de s'entendre dire qu'il n'y a pas
assez de m=E9moire pour faire ce qu'ils veulent ! Et dans ce cas-l=E0
encore, y'a-t-il une mani=E8re plus ou moins "standard" de v=E9rifier "=E0
la main" la m=E9moire disponible ?

Et accessoirement, j'imagine que diff=E9rents OS peuvent se comporter
diff=E9remment de ce point de vue, donc est-ce pareil pour SunOS ?
IRIX ? (les deux autres syst=E8mes que je suis susceptible d'utiliser)

Merci d'avance !
--
R=E9mi Moyen

10 réponses

Avatar
Rémi Moyen
On Feb 16, 5:30 pm, (Michel Talon) wrote:
Rémi Moyen wrote:

Oui enfin le problème du swap, c'est que je peux allouer la mémoire
par blocs de 1 Go ou 2. Dans ce cas, le temps de swapper assez pour me
faire de la place dans la RAM n'est pas négligeable et si je fais
cette opération pour rien (i.e. qu'à la fin de mon allocation de 1 Go
je me rends compte que je ne peux récupérer que 900 Mo et que mon t ruc
échoue), j'ai un peu gâché plein de temps.


Mais ce n'est pas parceque le noyau t'alloue 1 Go qu'il se dépêche de
swapper pour que tu puisses bénéficier de 1 Go en mémoire physique. En
fait il t'alloue 1 Go de mémoire *virtuelle* et c'est uniquement au
moment de s'en servir, page par page, qu'il va swapper des choses pour
faire de la place. Peut être qu'il n'aura même pas besoin de swapper du
tout parceque d'autres pages seront devenues naturellement libres, et
donc réutilisables directement sans avoir besoin de les copier en swap.
Donc j'ai l'impression que le problème que tu te poses est un faux
problème.


Peut-être, mais je n'ai pas l'impression :-)

Supposons que mon système a 2 Go de RAM et 4 Go de swap. À un instant
donné, y'a disons 3 Go sur le swap et toute la RAM d'occuppée. Je
demande au système 2 Go. Il va me dire "ok", parce qu'il ne réserve
rien à cet instant. Seulement dans mon cas, mes 2 Go, c'est un tableau
que je remplis immédiatement avec des valeurs. Donc je commence à
écrire, page par page, et (en supposant qu'aucun autre process ne
libère de mémoire par ailleurs) après avoir swappé 1 Go de RAM dans le
swap (enfin, y'a probablement des pages qui ne peuvent pas y passer,
mais peu importe), le système va voir que tout est plein et échouer.

Au choix, il tuera des trucs au hasard ou me retournera une erreur,
peu importe pour l'exemple. Ce qui compte, c'est qu'il n'échouera
qu'après avoir passé plein de temps à bouger des trucs dans le swap.
Si dès le début il me disait "attention, là à l'instant présent j 'ai
pas assez de place, mais peut-être que ça ira mieux plus tard", ça me
permettrait de ne pas essayer de remplir mon tableau et de remonter
une erreur à l'utilisateur. Bien sûr, je comprends bien que pendant
que je remplis mon tableau quelqu'un peut faire de la place ailleurs,
mais c'est assez peu probable que dans ce délai je gagne le Go qui
manque (je sais plus ou moins ce qui tourne sur ma machine et le
contexte d'utilisation).

Donc dans ce cas, j'aimerais avoir un moyen de voir avant d'allouer ma
mémoire si c'est complétement irréaliste ou si y'a une chance que ça
passe. La seule méthode que je voie, c'est d'aller compter à la main
free + cache + free swap avant chaque new() (en fait, y'a que quelques
cas où j'alloue des si gros blocs que j'utilise immédiatement, donc je
peux me contenter de surveiller ces cas-là) et de comparer ça à la
taille que je veux allouer pour éviter d'en demander trop si je sais,
de manière évidente, que ça ne passera pas. Je me trompe ?

D'où ma question initiale. Je voudrais juste être sûr que quand le
noyau commence à swapper des trucs, il ne le fait pas pour rien. Donc


Mais ce n'est pas toi qui gère ça, ni qui doit le gérer, c'est le n oyau
qui le fait en fonction de la connaissance qu'il a de l'ensemble des
requêtes au système de mémoire virtuelle. Depuis très longtemps s ous
FreeBSD et plus récemment sous Linux, il maintient des statistiques sur
l'utilisation des pages, et swappe les moins utilisées. Donc tu devrais
pouvoir compter sur le noyau pour résoudre optimalement ton problème
d'allocation de mémoire sans que tu ais à y penser en quoi que ce soi t.
Toute mesure que tu prendrais pour améliorer les choses risque de les
rendre pire, parceque dans ton raisonnement tu négliges complètement les
requêtes des autres processus.


Mais dans ce cas, je *sais* que mon programme va être le seul à
allouer/désallouer des blocs de mémoire de taille importante à ce
moment. Est-ce que ça n'est pas quand même utile d'essayer d'optimiser
un peu les choses pour éviter des écritures dont je suis certain
qu'elles vont droit dans le mur ? Je veux bien que, dans l'absolu,
l'OS sait mieux que moi ce qu'il faut faire, mais dans ce cas présent,
si l'OS me dit "oui mais dans 3 minutes tu auras peut-être ton Go
manquant", je peux lui répondre "bullshit !" tout de suite. Alors... ?

La bonne technique est d'avoir du swap
en surabondance pour éviter le problème à tout prix, et en second, de
dimensionner la ram pour que le système ne se mette à swapper que
rarement. Tu connais la taille maximale de ton programme, il te suffit
d'avoir un peu plus de ram (il faut penser aux autres programmes) et
le problème est résolu.


Et je fais comment si mon utilisateur a sous la main des jeux de
données d'une dizaine de Go et qu'il peut très bien décider d'en
charger l'un ou l'autre morceau à n'importe quel moment ? Je vais
avoir du mal à mettre 10 Go de RAM sur ma machine (d'autant plus que
ce jour-là, il aura un jeu de données de 11 Go, ou 20, ou plus). Il
faut d'une manière ou d'une autre que je limite ce qu'il peut charger.
Moi, j'espérais naïvement laisser le système faire le boulot et
récupérer les exceptions lancées par new() pour fixer la limite. Là,
je me rends compte qu'il va falloir que j'ajoute à la main un
méchanisme de contrôle à côté.

(note qu'en laissant faire new(), je me rends compte que je
n'envisageais pas vraiment le problème du swap, sinon que quand ça
commence à swapper, l'utilisateur se rend compte tout seul qu'il a
chargé trop de choses et il en enlève un peu :-) )

Merci pour tes explications détaillées !
--
Rémi Moyen


Avatar
Pascal Bourguignon
"Rémi Moyen" writes:
Peut-être, mais je n'ai pas l'impression :-)

Supposons que mon système a 2 Go de RAM et 4 Go de swap. À un instant
donné, y'a disons 3 Go sur le swap et toute la RAM d'occuppée. Je
demande au système 2 Go. Il va me dire "ok", parce qu'il ne réserve
rien à cet instant. Seulement dans mon cas, mes 2 Go, c'est un tableau
que je remplis immédiatement avec des valeurs. Donc je commence à
écrire, page par page, et (en supposant qu'aucun autre process ne
libère de mémoire par ailleurs) après avoir swappé 1 Go de RAM dans le
swap (enfin, y'a probablement des pages qui ne peuvent pas y passer,
mais peu importe), le système va voir que tout est plein et échouer.

Au choix, il tuera des trucs au hasard ou me retournera une erreur,
peu importe pour l'exemple. Ce qui compte, c'est qu'il n'échouera
qu'après avoir passé plein de temps à bouger des trucs dans le swap.
Si dès le début il me disait "attention, là à l'instant présent j'ai
pas assez de place, mais peut-être que ça ira mieux plus tard", ça me
permettrait de ne pas essayer de remplir mon tableau et de remonter
une erreur à l'utilisateur. Bien sûr, je comprends bien que pendant
que je remplis mon tableau quelqu'un peut faire de la place ailleurs,
mais c'est assez peu probable que dans ce délai je gagne le Go qui
manque (je sais plus ou moins ce qui tourne sur ma machine et le
contexte d'utilisation).


C'est un problème global, tu ne peux pas le résoudre au niveau d'un
processus. Si tu envisager de lire dans ton programme la sortie d'une
commande comme free(1) sur linux, ça ne marcherait pas à tous les
coup, car deux processus pourraient faire de même sans se rendre
compte qu'ils sont deux à demander 1 Go quand il n'y a qu'1 Go disponible.

C'est pour ça que ça ne peut être réglé que de l'extérieur, soit par
un opérateur qui sait ce qu'il fait, quels programmes il lance, ou par
un ordonanceur qui fasse la même chose mais automatiquement.


Donc dans ce cas, j'aimerais avoir un moyen de voir avant d'allouer ma
mémoire si c'est complétement irréaliste ou si y'a une chance que ça
passe. La seule méthode que je voie, c'est d'aller compter à la main
free + cache + free swap avant chaque new() (en fait, y'a que quelques
cas où j'alloue des si gros blocs que j'utilise immédiatement, donc je
peux me contenter de surveiller ces cas-là) et de comparer ça à la
taille que je veux allouer pour éviter d'en demander trop si je sais,
de manière évidente, que ça ne passera pas. Je me trompe ?


Non.


Mais dans ce cas, je *sais* que mon programme va être le seul à
allouer/désallouer des blocs de mémoire de taille importante à ce
moment. Est-ce que ça n'est pas quand même utile d'essayer d'optimiser
un peu les choses pour éviter des écritures dont je suis certain
qu'elles vont droit dans le mur ? Je veux bien que, dans l'absolu,
l'OS sait mieux que moi ce qu'il faut faire, mais dans ce cas présent,
si l'OS me dit "oui mais dans 3 minutes tu auras peut-être ton Go
manquant", je peux lui répondre "bullshit !" tout de suite. Alors... ?


Si tu sais que ton programme va être seul, tu dois aussi savoir
combien de mémoire tu as dans ton ordinateur et donc combien tu peux
allouer...


Et je fais comment si mon utilisateur a sous la main des jeux de
données d'une dizaine de Go et qu'il peut très bien décider d'en
charger l'un ou l'autre morceau à n'importe quel moment ? Je vais
avoir du mal à mettre 10 Go de RAM sur ma machine (d'autant plus que
ce jour-là, il aura un jeu de données de 11 Go, ou 20, ou plus). Il
faut d'une manière ou d'une autre que je limite ce qu'il peut charger.
Moi, j'espérais naïvement laisser le système faire le boulot et
récupérer les exceptions lancées par new() pour fixer la limite. Là,
je me rends compte qu'il va falloir que j'ajoute à la main un
méchanisme de contrôle à côté.


En effet. Si tu as des données à traiter en si grande quantité,
comparé à la taille de la mémoire, il faut en revenir aux anciennes
technique de travail avec des fichiers externes. Tu alloue un espace
de travail en RAM qui dépend de la taille de la mémoire disponible, et
tu ne charge dans cet espace de travail que des morceaux des données à
traiter. Il y a des algorithmes spécialement optimisés pour ce genre
de cas (par exemple, quick-sort NON, merge-sort OUI).
(voir par exemple http://en.wikipedia.org/wiki/Merge_Sort
"Merge sorting tape drives")

Bon, de nos jours, on peut améliorer un peu les choses, on peut
utiliser mmap pour éviter d'avoir à gérer les I/O soi même. Mais il
faut que le fichier soit structuré comme on le veut en RAM, et il faut
éviter les pointeurs (utiliser des déplacements à la place).
On peut alors mmap'er des blocs de 1Go ou 512Mo et traiter les 11 Go
morceau par morceau.


Sinon, il y a un truc marrant. Au fur et à mesure de l'évolution du
matériel informatique, on a une quantité qui reste approximativement
constante: le temps nécessaire pour scanner la mémoire centrale
(environ 1 seconde). Aussi bien pour scanner 64 Ko sur un 6502 à 512
KHz que pour scanner 1 Go sur un Athlon à 1 GHz, en passant par les
512 Ko à 6 MHz d'un 68000. Mais le temps d'accès à la mémoire de
masse ne s'améliorent pas tant, et le problème pour traiter des
grosses quantités de données, ce n'est pas tellement la taille de la
mémoire disponible, mais le temps qu'il faut pour les y charger. Même
avec mmap, et même si tu avais plus de RAM que de données, il te
faudra au moins dix minutes pour charger 10 Go de données à raison de
20 Mo/s soutenu. Alors avoir beaucoup de RAM n'est rentable que pour
des traitements multi-phase, où on peut ne charger les données qu'une
seule fois pour plusieurs traitements.

--
__Pascal Bourguignon__ http://www.informatimago.com/
Our enemies are innovative and resourceful, and so are we. They never
stop thinking about new ways to harm our country and our people, and
neither do we. -- Georges W. Bush

Avatar
Rémi Moyen
Pascal Bourguignon wrote:

Mais dans ce cas, je *sais* que mon programme va être le seul à
allouer/désallouer des blocs de mémoire de taille importante à ce
moment. Est-ce que ça n'est pas quand même utile d'essayer d'optimiser
un peu les choses pour éviter des écritures dont je suis certain
qu'elles vont droit dans le mur ? Je veux bien que, dans l'absolu,
l'OS sait mieux que moi ce qu'il faut faire, mais dans ce cas présent,
si l'OS me dit "oui mais dans 3 minutes tu auras peut-être ton Go
manquant", je peux lui répondre "bullshit !" tout de suite. Alors... ?


Si tu sais que ton programme va être seul, tu dois aussi savoir
combien de mémoire tu as dans ton ordinateur et donc combien tu peux
allouer...


Ben il va pas vraiment être "seul" (je tourne sur un desktop utilisé
donc aussi pour d'autres applis), donc je ne peux pas vraiment "tout"
prendre sans me poser de questions, d'autant plus que pour faire ça il
faut que je garde la trace de combien j'ai utilisé jusqu'à présent (que
je me fasse un garbage collector, quoi). Mais je sais que quand les
utilisateurs lancent mon programme, ils ne lancent généralement pas à
côté d'appli qui consomme de la mémoire en quantité comparable.

Bien sûr, rien ne me garantit qu'un jour un petit malin n'aura pas un
netscape en train de dégouliner de mémoire de partout et qui se fera
fermer au bon moment pour me libérer plein de mémoire. Mais si je dois
choisir entre systématiquement faire perdre 10 min à l'utilisateur avant
de lui dire "plus de mémoire", ou une fois de temps en temps lui dire
"plus de mémoire" alors qu'il y en aurait quand même eu le moment venu,
mon choix est très clair, c'est la 2ème solution.

C'est ce comportement que je voudrais obtenir.

Et je fais comment si mon utilisateur a sous la main des jeux de
données d'une dizaine de Go et qu'il peut très bien décider d'en
charger l'un ou l'autre morceau à n'importe quel moment ? Je vais
avoir du mal à mettre 10 Go de RAM sur ma machine (d'autant plus que
ce jour-là, il aura un jeu de données de 11 Go, ou 20, ou plus). Il
faut d'une manière ou d'une autre que je limite ce qu'il peut charger.
Moi, j'espérais naïvement laisser le système faire le boulot et
récupérer les exceptions lancées par new() pour fixer la limite. Là,
je me rends compte qu'il va falloir que j'ajoute à la main un
méchanisme de contrôle à côté.


En effet. Si tu as des données à traiter en si grande quantité,
comparé à la taille de la mémoire, il faut en revenir aux anciennes
technique de travail avec des fichiers externes. Tu alloue un espace
de travail en RAM qui dépend de la taille de la mémoire disponible, et
tu ne charge dans cet espace de travail que des morceaux des données à
traiter.


Oui, je sais bien. Mais l'idée était que, dans le cadre de ce programme,
l'utilisateur peut généralement faire un peu de place "à la main" quand
il a trop de trucs chargés (en réalité, il n'"utilise" pas vraiment
toutes ses données à un instant donné). Du coup, ça m'évitait de devoir
coder ce genre de trucs. D'autant plus que j'ai aussi ce genre de
systèmes, et que l'option de "charger en RAM" est justement là lorsque
l'utilisateur veut faire plein d'opérations sur un bloc de données et
qu'il les met explicitement en mémoire pour faciliter la vie au code de
chargement des données. Bref, ça colle pas vraiment avec mon contexte,
même si je suis d'accord que ça serait sans doute la meilleure solution
à la base.

Enfin, grâce à cette discussion je commence à clarifier un peu les
choses dans ma tête, aussi...
--
Rémi Moyen


Avatar
talon
Rémi Moyen wrote:
faire. Je suis censé hausser des épaules et dire "c'est normal", ou
y'a une stratégie propre pour éviter ça ???


Si j'en crois les bons auteurs que je t'ai cités, il n'y en a pas dans
la plupart des Unix. Ou le remède est pire que le mal. Ce qui veut dire
que la technique consistant à s'assurer que le new() est content est
parfaitement inutile. C'est encore une de ces idées de théoriciens de
la programmation qui n'a aucune traduction concrète dans la pratique.

--
Rémi Moyen



--

Michel TALON

Avatar
talon
Rémi Moyen wrote:

La question, c'est comment se comporte new()? Comment est elle
implémentée?


Je sais pas... c'est aussi la question que je me pose (et que je pose
aux gens ici...) !


New est implémenté à partir de malloc() en y ajoutant la surcouche
nécessaire pour gérer les objets (par exemple appliquer le constructeur)
mais certainement aucun raffinement supplémentaire de plus bas niveau
OS dépendant.
Si tu veux voir une discussion détaillée:
http://www.scs.stanford.edu/~dm/home/papers/c++-new.html
Je ne résiste pas au plaisir de te citer la conclusion (qu'on pourrait
sans peine appliquer à l'ensemble du langage):

Instead, Stroustrup introduced operator new, a disgusting piece of
syntax inadequate for the job. After many complaints, new's
functionality was finally extended to include per-class operator new[]
and placement new, still an imperfect solution.


--

Michel TALON


Avatar
Jean-Louis Liagre
Michel Talon wrote in message <er422f$1o3h$:
C'est le fameux problème de l'"overcommit".


Pas seulement.

Il y a deux niveaux :

- La noyau sait combien de mémoire virtuelle il lui reste, et si un
processus demande trop de mémoire, il lui répond non.

- Le noyau ne choisit pas immédiatement la mémoire qu'il attribue au
processus, il se contente de noter la quantité dans ses tables. Les pages
ne sont effectivement allouées que quand le processus y accède.

L'overcommit joue entre ces deux effets : si un processus demande beaucoup
mais utilise peu, alors les pages en rab peuvent être disponibles pour
d'autres processus.

banquiers qui prêtent de l'argent qu'ils n'ont pas


Légalement, les banquiers créent l'argent qu'ils prêtent.


Les banquiers ne tuent pas arbitrairement certains clients à qui ils
doivent de l'argent, contrairement à Linux et son "out of memory killer"
http://lwn.net/Articles/104179/ .

Je préfère rester sous Solaris.


Avatar
Nicolas George
Jean-Louis Liagre wrote in message
<45d6dda7$0$20038$:
Les banquiers ne tuent pas arbitrairement certains clients à qui ils
doivent de l'argent,


Mais Linux n'est pas capable de créer de la mémoire.

contrairement à Linux et son "out of memory killer"
http://lwn.net/Articles/104179/ .

Je préfère rester sous Solaris.


Tu ferais surtout bien d'apprendre à lire la doc, ainsi que les articles que
tu cites.

« Simply setting the sysctl parameter vm/overcommit_memory to 2 turns off
the overcommit behavior and keeps the OOM killer forever at bay. »

Avatar
Jean-Louis Liagre
Jean-Louis Liagre wrote in message
<45d6dda7$0$20038$:
Les banquiers ne tuent pas arbitrairement certains clients à qui ils
doivent de l'argent,


Mais Linux n'est pas capable de créer de la mémoire.


Dans la limite de l'espace disque disponible si. (swapon, swap -a ou
équivalent).

contrairement à Linux et son "out of memory killer"
http://lwn.net/Articles/104179/ .

Je préfère rester sous Solaris.


Tu ferais surtout bien d'apprendre à lire la doc, ainsi que les articles que
tu cites.




« Simply setting the sysctl parameter vm/overcommit_memory to 2 turns off
the overcommit behavior and keeps the OOM killer forever at bay. »



J'ai aussi lu:

http://developers.sun.com/solaris/articles/subprocess/subprocess.html
On y apprend que sous Linux, même quand l'overcommit est désactivé, il y
est quand même (!),


Avatar
Nicolas George
Jean-Louis Liagre wrote in message
<45d6e2dc$0$3702$:
Dans la limite de l'espace disque disponible si. (swapon, swap -a ou
équivalent).


Ce n'est pas créer, ça.

J'ai aussi lu:

http://developers.sun.com/solaris/articles/subprocess/subprocess.html
On y apprend que sous Linux, même quand l'overcommit est désactivé, il y
est quand même (!),


On y apprend surtout que Greg Nakhimovsky ne lit pas bien les docs.

Avatar
Michel Talon
Nicolas George wrote:

Jean-Louis Liagre wrote in message
<45d6e2dc$0$3702$:
Dans la limite de l'espace disque disponible si. (swapon, swap -a ou
équivalent).


Ce n'est pas créer, ça.

J'ai aussi lu:

http://developers.sun.com/solaris/articles/subprocess/subprocess.html
On y apprend que sous Linux, même quand l'overcommit est désactivé, il y
est quand même (!),


On y apprend surtout que Greg Nakhimovsky ne lit pas bien les docs.


Ou que les docs de Linux n'ont qu'un lointain rapport avec la réalité, ce
qui est fréquemment le cas, pour le problème en question je n'en sais rien.