2016-04-01 1 views
25

Est-ce qu'un compilateur peut faire conversion lvalue-à-valeur automatique s'il peut prouver que la lvalue ne sera plus utilisée? Voici un exemple pour clarifier ce que je veux dire:Est-ce qu'un compilateur optimiseur peut ajouter std :: move?

void Foo(vector<int> values) { ...} 

void Bar() { 
    vector<int> my_values {1, 2, 3}; 
    Foo(my_values); // may the compiler pretend I used std::move here? 
} 

Si un std::move est ajouté à la ligne commentée, le vecteur peut être déplacé dans le paramètre de Foo, plutôt que copier. Cependant, comme écrit, je n'ai pas utilisé std::move.

Il est assez facile de prouver statiquement que my_values ​​ne sera pas utilisé après la ligne commentée. Est-ce que le compilateur est autorisé à déplacer le vecteur, ou est-il nécessaire de le copier?

Répondre

32

Le compilateur doit se comporter comme si la copie est passée du vector à l'appel Foo.

Si le compilateur peut prouver qu'il ya est un comportement machine abstraite valide sans effets secondaires observables (dans le comportement de la machine abstraite, pas dans un véritable ordinateur!) Qui consiste à déplacer le std::vector dans Foo, il peut le faire.

Dans votre cas ci-dessus, ce mouvement n'a aucun effet secondaire visible sur la machine abstraite. le compilateur peut ne pas être capable de le prouver, cependant.

Le comportement observable éventuellement lors de la copie d'un std::vector<T> est:

  • invocation de constructeur de copie sur les éléments. Cela ne peut pas être observé avec int
  • Appel de la valeur par défaut std::allocator<> à différents moments. Cela appelle ::new et ::delete (peut-être) Dans tous les cas, ::new et ::delete n'a pas été remplacé dans le programme ci-dessus, donc vous ne pouvez pas observer cela sous la norme. Appeler le destructeur de T plus de fois sur différents objets. Non observable avec int.
  • Le vector étant non vide après l'appel à Foo. Personne ne l'examine, donc c'est vide comme si-ce n'était pas le cas.
  • Références ou pointeurs ou itérateurs aux éléments du vecteur extérieur étant différents de ceux à l'intérieur. Aucune référence, vecteur ou pointeur n'est pris aux éléments du vecteur à l'extérieur Foo.

Alors que vous pouvez dire « mais si le système est hors de la mémoire, et le vecteur est grand, est pas observable? »:

La machine abstraite n'a pas « de mémoire "condition, il a simplement l'allocation échouant parfois (lancer std::bad_alloc) pour des raisons non-contraintes. pas échec est un comportement valide de la machine abstraite, et ne pas manquer en n'allouant pas de mémoire (réelle) (sur l'ordinateur réel) est également valide, tant que la non-existence de la mémoire n'a pas d'effets secondaires observables.

Un peu plus cas de jouets:

int main() { 
    int* x = new int[std::size_t(-1)]; 
    delete[] x; 
} 

alors que ce programme attribue clairement trop de mémoire, le compilateur est libre de ne pas affecter quoi que ce soit.

Nous pouvons aller plus loin. Même:

int main() { 
    int* x = new int[std::size_t(-1)]; 
    x[std::size_t(-2)] = 2; 
    std::cout << x[std::size_t(-2)] << '\n'; 
    delete[] x; 
} 

peut être transformé en std::cout << 2 << '\n';. Ce grand tampon doit exister de façon abstraite, mais tant que votre "vrai" programme se comporte comme si la machine abstraite le faisait, il ne doit pas réellement l'allouer. Malheureusement, il est difficile de le faire à toute échelle raisonnable. Il y a beaucoup de façons dont les informations peuvent fuir à partir d'un programme C++. Donc, compter sur de telles optimisations (même si elles se produisent) ne va pas bien se terminer.


Il y avait des trucs sur coalescent appels à new qui pourraient embrouiller la question, je ne suis pas certain si ce serait légal de passer des appels même s'il y avait un ::new remplacé.


Un fait important est qu'il ya des situations que le compilateur est pas nécessaire de se comporter comme s'il y avait-une copie, même si std::move n'a pas été appelé.

Lorsque vous return une variable locale à partir d'une fonction dans une ligne qui ressemble return X; et X est l'identifiant, et en ce que la variable locale est d'une durée de stockage automatique (sur la pile), l'opération est implicitement un mouvement, et la compilateur (si possible) peut élider l'existence de la valeur de retour et la variable locale dans un objet (et même omettre le move).

La même chose est vraie lorsque vous construisez un objet à partir d'un temporaire - l'opération est implicitement un mouvement (car il se lie à une valeur) et il peut se débarrasser complètement du mouvement. Dans les deux cas, le compilateur doit le traiter comme un mouvement (et non comme une copie), et il peut éviter le déplacement.

std::vector<int> foo() { 
    std::vector<int> x = {1,2,3,4}; 
    return x; 
} 

que x n'a pas std::move, mais il est déplacé dans la valeur de retour, et cette opération peut être élidée (x et la valeur de retour peut être transformé en un objet).

Ce:

std::vector<int> foo() { 
    std::vector<int> x = {1,2,3,4}; 
    return std::move(x); 
} 

blocs élision, tout comme ceci:

std::vector<int> foo(std::vector<int> x) { 
    return x; 
} 

et nous pouvons même bloquer le mouvement:

std::vector<int> foo() { 
    std::vector<int> x = {1,2,3,4}; 
    return (std::vector<int> const&)x; 
} 

ou même:

std::vector<int> foo() { 
    std::vector<int> x = {1,2,3,4}; 
    return 0,x; 
} 

car les règles de déplacement implicite sont intentionnellement fragiles. (0,x est une utilisation de l'opérateur , tant décrié). Maintenant, se fier au déplacement implicite qui ne se produit pas dans ce cas , n'est pas conseillé: le comité standard a déjà changé un cas de copie implicite en un déplacement implicite puisque le déplacement implicite a été ajouté au langage car ils l'ont jugé inoffensif (où la fonction renvoie un type A avec un ctor A(B&&), et l'instruction de retour est return b;b est de type B; à la version C++ 11 qui a fait une copie, maintenant il fait un mouvement.) L'expansion d'implicit-move ne peut pas être exclue: le fait de lancer explicitement un const& est probablement le moyen le plus fiable de l'empêcher maintenant et dans le futur.

+1

Avez-vous des liens où je peux en apprendre plus sur comment cela fonctionne ou comment la machine abstraite est définie? – vu1p3n0x

+2

@ vu1p3n0x Trouver une copie de la norme C++? [En voici un brouillon] (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf). [En voici une autre] (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3376.pdf). Le comportement de C++ est spécifié dans la norme en termes de machine abstraite. – Yakk

+0

Réponse parfaite. –

2

Dans ce cas, le compilateur peut être déplacé de my_values. C'est parce que cela ne provoque aucune différence dans comportement observable.

Citant le C++ la définition de normes de comportement observable:

Les exigences les moins sur une mise en œuvre conforme sont:

  • L'accès aux objets volatils sont évalués strictement selon les règles de la machine abstraite.
  • À la fin du programme, toutes les données écrites dans les fichiers doivent être identiques à l'un des résultats possibles que l'exécution du programme selon la sémantique abstraite aurait produit.
  • La dynamique d'entrée et de sortie des dispositifs interactifs doit être telle que la sortie d'invitation soit réellement délivrée avant qu'un programme n'attende une entrée. Ce qui constitue un périphérique interactif est défini par l'implémentation.

L'interprétation de ce peu: « fichiers » ici inclut le flux de sortie standard, et pour les appels de fonctions qui ne sont pas définies par la norme C++ (par exemple, les appels système d'exploitation, ou des appels à des bibliothèques de tiers), il Il faut supposer que ces fonctions peuvent écrire dans un fichier, ce qui a pour corollaire que les appels de fonctions non standard doivent également être considérés comme des comportements observables.

Cependant, votre code (comme vous l'avez montré) n'a aucune variable volatile et aucun appel à des fonctions non standard. Ainsi, les deux versions (move ou not-move) doivent avoir un comportement observable identique et donc le compilateur peut faire l'une ou l'autre (ou même optimiser complètement la fonction, etc.En pratique, bien sûr, il n'est généralement pas si facile pour un compilateur de prouver qu'aucun appel de fonction non standard ne se produit, tant d'opportunités d'optimisation de ce type sont manquées. Par exemple, dans ce cas, le compilateur ne sait pas encore si la valeur par défaut ::operator new a été remplacée par une fonction qui génère une sortie.