Je suis tombé sur une situation où il serait utile d'avoir des appels inutiles à realloc
en cours d'optimisation. Cependant, il semble que ni clang ni gcc ne le fassent ( godbolt ). - Bien que des optimisations soient effectuées avec plusieurs appels à malloc
.
L'exemple:
void *myfunc() {
void *data;
data = malloc(100);
data = realloc(data, 200);
return data;
}
Je m'attendais à ce qu'il soit optimisé comme suit:
void *myfunc() {
return malloc(200);
}
Pourquoi ni clang ni gcc ne l’optimisent-ils? - Ne sont-ils pas autorisés à le faire?
Ne sont-ils pas autorisés à le faire?
Peut-être, mais l'optimisation non effectuée dans ce cas peut être due à des différences fonctionnelles de coin.
S'il reste 150 octets de mémoire pouvant être allouée,data = malloc(100); data = realloc(data, 200);
renvoie NULL
avec 100 octets consommés (et ayant fui) et 50 restants.
data = malloc(200);
renvoie NULL
avec 0 octet consommé (aucune fuite) et 150 restants.
Une fonctionnalité différente dans ce cas étroit peut empêcher l'optimisation.
Les compilateurs sont-ils autorisés à optimiser-out realloc?
Peut-être - je m'attendrais à ce que ce soit autorisé. Cependant, l’effet d’améliorer le compilateur pour en déterminer le moment peut ne pas valoir la peine.
malloc(n); ... realloc(p, 2*n)
réussi diffère de malloc(2*n);
lorsque ...
peut avoir défini une partie de la mémoire.
Cela pourrait aller au-delà de la conception de ce compilateur pour s'assurer que ...
, même si le code est vide, ne définit aucune mémoire.
Un compilateur qui regroupe ses propres versions autonomes de malloc/calloc/free/realloc pourrait légitimement effectuer l'optimisation indiquée si les auteurs estimaient que cela en valait la peine. Un compilateur qui se connecte à des fonctions fournies de l'extérieur peut toujours effectuer de telles optimisations s'il constate qu'il ne considère pas la séquence précise d'appels à ces fonctions comme un effet secondaire observable, mais un tel traitement pourrait être un peu plus ténu.
Si aucun stockage n'est alloué ou désalloué entre malloc () et realloc (), la taille de realloc () est connue lorsque malloc () est exécuté et la taille de realloc () est supérieure à celle de malloc (), il peut être judicieux de consolider les opérations malloc () et realloc () en une seule allocation plus grande. Cependant, si l'état de la mémoire pouvait changer dans l'intervalle, une telle optimisation pourrait entraîner l'échec des opérations qui auraient dû aboutir. Par exemple, étant donné la séquence:
void *p1 = malloc(2000000000);
void *p2 = malloc(2);
free(p1);
p2 = realloc(p2, 2000000000);
un système peut ne pas avoir 2000000000 octets disponibles pour p2 avant la libération de p1. Si cela devait changer le code en:
void *p1 = malloc(2000000000);
void *p2 = malloc(2000000000);
free(p1);
cela entraînerait l'échec de l'allocation de p2. Comme la norme ne garantit jamais que les demandes d'allocation aboutiront, un tel comportement ne serait pas non conforme. D'autre part, ce qui suit serait aussi une implémentation "conforme":
void *malloc(size_t size) { return 0; }
void *calloc(size_t size, size_t count) { return 0; }
void free(void *p) { }
void *realloc(void *p, size_t size) { return 0; }
Une telle implémentation pourrait sans doute être considérée comme plus "efficace" que la plupart des autres, mais il faudrait être assez obtuse pour la considérer comme très utile, sauf peut-être dans de rares cas où les fonctions ci-dessus sont appelées sur des chemins de code qui sont jamais exécuté.
Je pense que la norme permettrait clairement l'optimisation, au moins dans des cas aussi simples que ceux de la question initiale. Même dans les cas où cela risquerait d'entraîner l'échec d'opérations qui auraient autrement pu aboutir, la norme le permettait toujours. Très probablement, la plupart des compilateurs n'effectuent pas l'optimisation, c'est que les auteurs ne pensaient pas que les avantages seraient suffisants pour justifier l'effort requis pour identifier les cas où cela serait sûr et utile.
Le compilateur est autorisé à optimiser plusieurs appels à des fonctions considérées comme des fonctions pures, c’est-à-dire des fonctions n’ayant aucun effet secondaire.
La question est donc de savoir si realloc()
est une fonction pure ou non.
La version préliminaire N1570 du Comité de la norme C11 énonce ceci à propos de la fonction realloc
:
7.22.3.5 La fonction realloc
...
2. La fonctionrealloc
libère l'ancien objet pointé par ptr et renvoie un pointeur sur un nouvel objet dont la taille est spécifiée par taille. Le contenu du nouvel objet doit être identique à celui de l'ancien objet avant la désallocation, jusqu'à la plus petite des tailles nouvelles et anciennes. Tous les octets du nouvel objet dépassant la taille de l'ancien ont des valeurs indéterminées.Résultats
4. La fonctionrealloc
renvoie un pointeur sur le nouvel objet (qui may peut avoir la même valeur qu'un pointeur sur l'ancien objet) ou un pointeur null si le nouvel objet ne peut pas être attribué.
Notez que le compilateur ne peut pas prédire au moment de la compilation la valeur du pointeur qui sera renvoyé à chaque appel.
Cela signifie que realloc()
ne peut pas être considéré comme une fonction pure et que le compilateur ne peut pas optimiser ses appels multiples.
Mais vous ne vérifiez pas la valeur de retour du premier malloc () que vous utilisez ensuite dans le second realloc (). Cela pourrait tout aussi bien être NULL.
Comment le compilateur pourrait-il optimiser les deux appels en un seul sans faire d'hypothèses injustifiées sur la valeur de retour du premier?
Ensuite, il y a un autre scénario possible. FreeBSD avait auparavant une realloc()
qui était essentiellement malloc + memcpy + libère l’ancien pointeur.
Supposons qu'il ne reste que 230 octets de mémoire libre. Dans cette implémentation, ptr = malloc(100)
suivi de realloc(ptr, 200)
échouera, mais un seul malloc(200)
aboutira.
Si j'ai bien compris, une telle optimisation pourrait être interdite (notamment dans le cas - peu probable - où la variable malloc
réussit, mais la variable realloc
suivante échoue).
Vous pouvez supposer que malloc
et realloc
réussissent toujours (c'est contre le standard C11, n1570 ; regardez aussi dans mon plaisanterie-implémentation de malloc
). Dans cette hypothèse (stricto sensu, mais certains systèmes Linux ont un excès de mémoire pour donner cette illusion), si vous utilisez GCC , vous pourriez écrire votre propre plugin GCC pour créer un tel une optimisation.
Je ne suis pas sûr que cela vaut la peine de passer quelques semaines ou quelques mois à coder un tel plugin GCC (en pratique, vous voulez probablement qu'il gère parfois du code entre malloc
et realloc
, et ce n'est pas si simple, car vous devez caractériser et détecter quel code intermédiaire est acceptable), mais ce choix vous appartient.