67 votes

Le compilateur est-il autorisé à optimiser les allocations de mémoire de type heap ?

Considérons le code simple suivant qui fait usage de new (Je suis conscient qu'il n'y a pas delete[] mais cela n'a rien à voir avec cette question) :

int main()
{
    int* mem = new int[100];

    return 0;
}

Le compilateur est-il autorisé à optimiser le new appeler ?

Dans mes recherches, g++ (5.2.0) et Visual Studio 2015 n'optimisent pas les new appeler, alors que clang (3.0+) fait . Tous les tests ont été effectués avec les optimisations complètes activées (-O3 pour g++ et clang, mode Release pour Visual Studio).

N'est-ce pas ? new faire un appel système sous le capot, rendant impossible (et illégal) pour un compilateur de l'optimiser ?

EDIT : J'ai maintenant exclu les comportements non définis du programme :

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[100];
    return 0;
}

clang 3.0 n'optimise pas cela. plus, mais les versions ultérieures le font .

EDIT2 :

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[1000];

    if (mem != 0)
      return 1;

    return 0;
}

clang retourne toujours 1 .

53voto

Shafik Yaghmour Points 42198

L'histoire ici semble être que clang suit les règles établies dans N3664 : Clarification de l'allocation de mémoire ce qui permet au compilateur d'optimiser les allocations de mémoire, mais en tant que Nick Lewycky fait remarquer :

Shafik a fait remarquer que cela semble violer la causalité mais N3664 a commencé sa vie comme N3433, et je suis presque sûr que nous avons écrit l'optimisation d'abord et le papier ensuite de toute façon.

Clang a donc mis en œuvre l'optimisation, qui est devenue par la suite une proposition mise en œuvre dans le cadre de C++14.

La question de base est de savoir s'il s'agit d'une optimisation valable avant l'entrée en vigueur de la directive. N3664 C'est une question difficile. Nous devrions aller à la règle "as-if couverte dans la section du projet de norme C++ 1.9 Exécution du programme qui dit( c'est moi qui souligne ) :

Les descriptions sémantiques contenues dans la présente Norme internationale définissent une machine abstraite non déterministe paramétrée. La présente Norme internationale internationale n'impose aucune exigence sur la structure des implémentations implémentations conformes. En particulier, elles n'ont pas besoin de copier ou d'émuler la structure de la machine abstraite. structure de la machine abstraite. Au contraire, Les implémentations conformes doivent émuler (seulement) le comportement observable de la machine abstraite. abstraite comme expliqué ci-dessous. 5

où note 5 dit :

Cette disposition est parfois appelée Règle du "comme si". car une l'implémentation est libre de ne pas tenir compte d'une exigence de cette norme internationale, pour autant que le résultat soit le même que si l'exigence avait été respectée, dans la mesure où l'on peut le déterminer à partir du comportement comportement observable du programme. Par exemple, une implémentation réelle ne doit pas n'a pas besoin d'évaluer une partie d'une expression si elle peut déduire que sa valeur n'est pas utilisée et qu'aucun effet secondaire n'affecte le programme. valeur n'est pas utilisée et qu'aucun effet secondaire affectant le comportement observable du du programme.

Depuis new pourrait lever une exception qui aurait un comportement observable puisqu'elle modifierait la valeur de retour du programme.

On pourrait argumenter qu'il s'agit d'un détail d'implémentation pour savoir quand lever une exception et donc que clang pourrait décider que même dans le scénario ne causerait pas d'exception et donc éluder l'argument de l'exception. new ne violerait pas la règle "as-if .

Si nous utilisons la version qui ne lance pas, l'optimisation de la nouvelle version ne semble pas affecter le comportement observable.

Mais nous pourrions avoir un opérateur global de remplacement new dans une unité de traduction différente, ce qui affecterait le comportement observable. Le compilateur devrait donc avoir un moyen de prouver que ce n'est pas le cas, sinon il ne serait pas en mesure d'effectuer cette optimisation sans violer la règle de l'utilisateur. règle "as-if . Les versions précédentes de clang optimisaient en effet dans ce cas comme cet exemple de godbolt montre qui a été fourni via Casey ici en prenant ce code :

#include <cstddef>

extern void* operator new(std::size_t n);

template<typename T>
T* create() { return new T(); }

int main() {
    auto result = 0;
    for (auto i = 0; i < 1000000; ++i) {
        result += (create<int>() != nullptr);
    }

    return result;
}

et l'optimiser en fonction de cela :

main:                                   # @main
    movl    $1000000, %eax          # imm = 0xF4240
    ret

Cela semble en effet beaucoup trop agressif, mais les versions ultérieures ne semblent pas le faire.

19voto

sbabbi Points 3366

Ceci est autorisé par N3664 .

Une implémentation est autorisée à omettre un appel à une fonction d'allocation globale remplaçable (18.6.1.1, 18.6.1.2). Dans ce cas, le stockage est fourni par l'implémentation ou par l'extension de l'allocation d'une autre nouvelle expression.

Cette proposition fait partie de la norme C++14, donc en C++14 le compilateur est autorisé à optimiser un new (même si elle peut être rejetée).

Si vous jetez un coup d'œil à la Statut de l'implémentation de Clang il est clairement indiqué qu'ils appliquent la norme N3664.

Si vous observez ce comportement en compilant en C++11 ou C++03, vous devez remplir un bug.

Notez qu'avant le C++14, les allocations de mémoire dynamique font partie de l'état observable du programme (bien que je ne puisse pas trouver de référence à ce sujet pour le moment), donc une implémentation conforme n'était pas autorisée à appliquer la méthode d'évaluation de l'impact sur l'environnement. as-if dans ce cas.

9voto

N'oubliez pas que la norme C++ indique ce qu'un programme correct doit faire, et non comment il doit le faire. Elle ne peut pas du tout dire ce qu'il faut faire car de nouvelles architectures peuvent apparaître et apparaissent après l'écriture de la norme et la norme doit leur être utile.

new ne doit pas nécessairement être un appel système sous le capot. Il existe des ordinateurs utilisables sans système d'exploitation et sans notion d'appel système.

Par conséquent, tant que le comportement final ne change pas, le compilateur peut optimiser tout et n'importe quoi. Y compris ce qui new

Il y a une mise en garde.
Un opérateur global de remplacement new aurait pu être défini dans une unité de traduction différente.
Dans ce cas, les effets secondaires de la nouvelle pourraient être tels qu'ils ne peuvent être optimisés. Mais si le compilateur peut garantir que l'opérateur new n'a pas d'effets secondaires, comme ce serait le cas si le code affiché était le code entier, alors l'optimisation est valide.
Le fait que new puisse lancer std::bad_alloc n'est pas une exigence. Dans ce cas, lorsque new est optimisé, le compilateur peut garantir qu'aucune exception ne sera levée et qu'aucun effet secondaire ne se produira.

7voto

Damon Points 26437

Il est parfaitement permis (mais pas nécessaire ) pour qu'un compilateur optimise les allocations dans votre exemple original, et encore plus dans l'exemple EDIT1 selon le §1.9 de la norme, qui est généralement appelé le règle "as-if :

Les implémentations conformes doivent émuler (uniquement) le comportement observable de la machine abstraite, comme expliqué ci-dessous :
[3 pages de conditions]

Une représentation plus lisible par l'homme est disponible à l'adresse suivante cppreference.com .

Les points pertinents sont les suivants :

  • Vous n'avez pas de volatiles, donc les points 1) et 2) ne s'appliquent pas.
  • Vous ne sortez/écrivez aucune donnée et n'invitez pas l'utilisateur, donc 3) et 4) ne s'appliquent pas. Mais même si c'était le cas, ils seraient clairement satisfaits dans EDIT1 (on peut dire que également dans l'exemple original, bien que d'un point de vue purement théorique, il soit illégal puisque le déroulement et la sortie du programme - théoriquement - diffèrent, mais voir deux paragraphes ci-dessous).

Une exception, même si elle n'est pas attrapée, est un comportement bien défini (pas indéfini !). Cependant, strictement parlant, dans le cas où new (ce qui n'arrivera pas, voir aussi le paragraphe suivant), le comportement observable serait différent, à la fois par le code de sortie du programme et par toute sortie qui pourrait suivre plus tard dans le programme.

Maintenant, dans le cas particulier d'une petite allocation singulière, vous pouvez donner au compilateur l'adresse suivante "bénéfice du doute" qu'il peut garantie que l'allocation n'échouera pas.
Même sur un système soumis à une très forte pression mémoire, il n'est pas possible de démarrer un processus lorsque la granularité d'allocation disponible est inférieure à la granularité minimale. main également. Ainsi, si cette allocation devait échouer, le programme ne pourrait jamais démarrer ou aurait déjà connu une fin peu gracieuse avant que main est même appelé.
Dans la mesure où, en supposant que le compilateur le sache, même si l'allocation pourrait en théorie jeter il est même légal d'optimiser l'exemple original, puisque le compilateur peut pratiquement garantir que cela ne se produira pas.

<sérieusement indécis>
D'autre part, il est pas Il est possible (et comme vous pouvez le constater, c'est un bug du compilateur) d'optimiser l'allocation dans votre exemple EDIT2. La valeur est consommée pour produire un effet observable de l'extérieur (le code de retour).
Notez que si vous remplacez new (std::nothrow) int[1000] avec new (std::nothrow) int[1024*1024*1024*1024ll] (c'est une allocation de 4 To !), ce qui est -- sur les ordinateurs actuels -- garanti d'échouer, il optimise quand même l'appel. En d'autres termes, il renvoie 1 alors que vous avez écrit un code qui doit renvoyer 0.

@Yakk a soulevé un bon argument contre cela : Tant que la mémoire n'est jamais touchée, un pointeur peut être renvoyé, et aucune RAM réelle n'est nécessaire. En ce sens, il serait même légitime d'optimiser l'allocation dans EDIT2. Je ne suis pas sûr de savoir qui a raison et qui a tort ici.

Une allocation de 4 To est pratiquement garantie d'échouer sur une machine qui ne dispose pas d'une quantité de RAM d'au moins deux gigaoctets, simplement parce que le système d'exploitation doit créer des tables de pages. Bien sûr, la norme C++ ne se soucie pas des tables de pages ou de ce que le système d'exploitation fait pour fournir de la mémoire, c'est vrai.

Mais d'un autre côté, l'hypothèse "ça va marcher si on ne touche pas à la mémoire" s'appuie sur sur un tel détail et sur quelque chose que l'OS fournit. L'hypothèse selon laquelle si la RAM n'est pas touchée, c'est qu'elle n'est pas nécessaire, n'est vraie que dans les cas suivants parce que le système d'exploitation fournit de la mémoire virtuelle. Et cela implique que le système d'exploitation doit créer des tables de pages (je peux prétendre que je ne suis pas au courant, mais cela ne change rien au fait que je m'y fie quand même).

Par conséquent, je pense qu'il n'est pas correct à 100% de supposer d'abord l'un et de dire ensuite "mais nous ne nous soucions pas de l'autre".

Donc, oui, le compilateur peut supposer qu'une allocation de 4TiB est en général parfaitement possible tant que la mémoire n'est pas touchée, et qu'elle peut supposer qu'il est généralement possible de réussir. Il pourrait même supposer qu'il est probable de réussir (même si ce n'est pas le cas). Mais je pense que dans tous les cas, on n'est jamais autorisé à supposer que quelque chose doit travailler quand il y a une possibilité d'échec. Et non seulement il y a une possibilité d'échec, mais dans cet exemple, l'échec est même l'élément le plus important. plus probable possibilité.
</i> Légèrement indécis>

2voto

Quentin Points 3904

Le pire qui puisse arriver dans votre extrait est que new jette std::bad_alloc qui n'est pas traité. Ce qui se passe alors est défini par l'implémentation.

Le meilleur cas étant un no-op et le pire cas n'étant pas défini, le compilateur est autorisé à les prendre en compte pour qu'ils n'existent pas. Maintenant, si vous essayez réellement et attrapez l'exception possible :

int main() try {
    int* mem = new int[100];
    return 0;
} catch(...) {
  return 1;
}

... alors l'appel à operator new est conservé .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X