7
votes

Pourquoi cette boucle no-op n'est-elle pas optimisée?

Le code suivant effectue certaines copies d'un tableau de zéros interprétés comme floats à un autre et imprime la synchronisation de cette opération. Comme je l'ai vu de nombreux cas où les boucles de non-Op sont simplement optimisées par les compilateurs, y compris GCC, j'attendais que, à un moment donné de changer mon programme de tableaux de copie, il arrête de faire la copie.

#include <iostream>
#include <cstring>
#include <sys/time.h>

static inline long double currentTime()
{
    timespec ts;
    clock_gettime(CLOCK_MONOTONIC,&ts);
    return ts.tv_sec+(long double)(ts.tv_nsec)*1e-9;
}

int main()
{
    size_t W=20000,H=10000;

    float* data1=new float[W*H];
    float* data2=new float[W*H];
    memset(data1,0,W*H*sizeof(float));
    memset(data2,0,W*H*sizeof(float));

    long double time1=currentTime();
    for(int q=0;q<16;++q) // take more time
        for(int k=0;k<W*H;++k)
            data2[k]=data1[k];
    long double time2=currentTime();

    std::cout << (time2-time1)*1e+3 << " ms\n";

    delete[] data1;
    delete[] data2;
}


11 commentaires

Ce n'est pas de notre point de vue, mais le compilateur ne connaît pas et ne peut pas savoir que le contenu du morceau de mémoire à partir de données1 et de données2 est déjà égal.


Je ne peux pas donner une réponse définitive, mais a) toute optimisation n'est pas obligatoire. b) Vous avez deux Nouveau mais aucun Supprimer .


@deviantfan, je comptais sur la mémoire libérée à la sortie, bien qu'il ne semble pas être garanti par la norme.


Même si des systèmes d'exploitation majeurs peuvent faire cela, il est tout simplement faux d'omettre les supports. Et pour tout ce que je sais, cela pourrait même résoudre votre problème d'optimisation ("pourrait", pas "volonté").


@deviantfan j'ai vérifié, ça ne le fait pas.


@ PAN-: il sait cependant que le résultat de la calcul de données2 n'est pas utilisé. Je suis d'accord avec Op, ce type de code est généralement optimisé dans ma pratique et je dois prendre des mesures particulières pour m'assurer que cela ne se produit pas lors de la rédaction de ces points de repère.


Je suis curieux de le savoir. Voulez-vous donner une meilleure chance de combattre si elles ne sont pas dynamiques? Odd, d'accord, mais sérieusement, vraiment curieux (je penserais que cela ne fait aucune différence, mais la peine de découvrir: statique ).


VLAS? Ce n'est même pas portable C ++ .. et conservez le code d'origine (aussi?) C'est déroutant si la question change à la volée.


Si vous déclarez w, h comme consexpr g ++ optimise votre code. Ceci, cependant, n'est pas une réponse à votre question.


Pourquoi continuez-vous à changer l'allocation de la matrice? Votre version actuelle n'est même pas la zéro des tableaux; Ce n'est pas une surprise que le compilateur n'oblise pas la boucle loin.


@ user2357112 Il s'agissait des tentatives de simplification du code maximum


4 Réponses :


0
votes

Pourquoi vous attendez-vous au compilateur d'optimiser cela? Il est généralement très difficile de prouver que les écrivies d'adresses de mémoire arbitraires sont un "no-op". Dans votre cas, il serait possible, mais il faudrait que le compilateur trace les adresses de mémoire en tas par nouveau (qui est à nouveau dur puisque ces adresses sont générées au moment de l'exécution) et il n'y a vraiment aucune incitation à ce que ce soit.

Après tout, vous dites au compilateur explicitement que vous souhaitez allouer la mémoire et à y écrire. Comment le compositeur pauvre est-il de savoir que vous avez menté à cela?

En particulier, le problème est que la mémoire du tas pourrait être aliasée à beaucoup d'autres choses. Il se trouve être privé à votre processus, mais comme je l'ai dit ci-dessus, prouvant qu'il s'agit de beaucoup de travail pour le compilateur, contrairement à la mémoire de la fonction locale.


17 commentaires

Il n'y a plus de référence à cette mémoire, c'est comment.


Je m'attends à ce que le compilateur vérifie les dépendances de données et ne dis que "je ne sais pas" sur volatile mémoire. Ainsi, il semble assez naturel d'attendre une optimisation assez agressive.


@Violetgirafe le prouver. Astuce: C'est vraiment, vraiment difficile.


Dans ce cas, c'est vraiment, vraiment simple.


@MarkusMayR volatile signifie également que tout écrit à celui-ci doit être dans l'ordre prescrit et compter. Cela peut être un pointeur à une région MMIO.


@Konradrudolph Quoi qu'il en soit, vos préoccupations concernant le nouveau ne semblent pas être (entièrement) valables. J'ai mis à jour la question en supprimant les deux Memset et nouveaux opérateurs.


@Ruslan je ne peux pas croire que cela fonctionne et ne se sépare pas. Le tableau est de Waaay à gros pour être géré sur la pile.


@freakish Oui, ulimit est le traitement


@Ruslan ah, bien sûr. Retour au sujet: essayez-vous de résoudre un problème réel? Sinon, ne gardez pas à l'esprit que probablement personne ne sera probablement en mesure de répondre à cette question. Sauf pour les personnes qui ont écrit le compilateur. Peut-être que vous devriez leur demander directement?


@freakish, j'essaie simplement de comprendre la logique à propos de savoir quand s'attendre à ce que certaines optimisations ruinent la référence et quand on s'attend à ce que cela fonctionne. Depuis la réponse de Manlio, il semble que Clang optimise cette optimisation.


@Ruslan Oui, c'est pourquoi j'ai dit que vous devriez demander aux personnes qui ont créé G ++ ( La liste de diffusion ). Ce n'est pas un problème général. Le problème est spécifique à g ++. De plus, je pense que vous devriez supposer le moins possible sur les optimisations. Off-sujets: Clang est un très bon compilateur, vous devriez l'essayer.


@freakish peut-être bon, mais pas assez bon pour pouvoir remplacer GCC partout.


@Ruslan je ne suis pas convaincu qu'Invira ma réponse. Cela montre simplement que même sur la pile Le compilateur n'opère tout simplement pas aussi loin que vous le pensez.


@KonRADRUDOLPH Bien sûr, je n'ai pas dit que cela doit optimiser. Je me demandais si cela ne devait pas.


@Karolyhorvath Non, c'est tout simplement faux, à moins que le compilateur traite à la fois opérateur Nouveau et MEMSET comme intrinsique (qu'il est autorisé à faire, mais cela ne signifie souvent pas).


Eh bien, tout ce qu'il faut savoir, c'est que la mémoire n'est pas aliasée. Je m'attendrais à ce que beaucoup du compilateur (à la fois pour le nouveau et le memset). Après cela, c'est une déduction simple.


"Après tout, vous dites au compilateur explicitement que vous voulez allouer de la mémoire et écrivez-y. Comment le pauvre compilateur est-il de savoir que vous allez mentir?" - Tout comme vous explicitement Dites au compilateur d'allouer une variable locale et d'écrire à elle. et puis jamais l'utiliser à nouveau. Est-ce un mensonge?



0
votes

Le seul moyen de savoir que le compilateur pouvait savoir qu'il s'agit d'un non-op est s'il savait que memset fait. Pour que cela se produise, la fonction doit être définie dans un en-tête (et elle n'est généralement pas), soit elle doit être traitée comme une intrinsèque spéciale par le compilateur. Mais sauf ces astuces, le compilateur vient d'appeler une fonction inconnue que pourrait avoir des effets secondaires et faire des choses différentes pour chacun des deux appels.


6 commentaires

Je dirais que la question ne concerne pas principalement le memset , mais sur la data2 [k] = data1 [k] boucle (car data2 n'est plus accessible après cela).


Commentaire Memset Les appels ne changent pas de comportement (bien que 6950 ms modifient à 5500 ms pour une raison quelconque).


Le même argument détient pour opérateur nouveau . Et il n'y a pas vraiment de motivation pour faire une allocation de mémoire un compilateur intrinsèque afin que le compilateur ne connaisse probablement pas toutes les subtilités de la procédure d'allocation de mémoire du système d'exploitation.


@KonRADRUDOLPH: En fait, au-delà de l'utilisation intrinsique, les compilateurs trichent. Par exemple dans LLVM, MALLOC et gratuit (et les noms mutionnés de Nouveau et Suppr ) sont connus de la Optimiseur de sorte que si elle se rend compte que la mémoire est inutilisée (après optimisation par exemple), elle optimise également l'allocation / la répartition. Ceci enfreint tout ce qui est en violation de tout instrument qui dépendrait des effets secondaires (et avec ld_library_preload ils ne peuvent pas savoir à ce sujet), mais c'est une stratégie utilisée.


@Matthieuum. Bien sûr qu'ils font. Je voulais dire que vous ne pouvez pas attendre tricher. Je suis légèrement surpris que LLVM se déroule dans MALLOC et gratuit mais d'accord.


@KonRADRUDOLPH: Je suis également surpris aussi, je ne sais pas s'il existe un libellé spécifique dans la norme pour permettre leur élision (comme pour les constructeurs de copies), je n'ai certainement jamais rien vu mentionné. Je sais aussi que LLVM pourrait modifier également une allocation de tas à une étalonnage.



8
votes

Quoi qu'il en soit, ce n'est pas impossible (Clang ++ version 3.3):

...
const size_t W=20000;
const size_t H=10000;

float data1[W*H];
float data2[W*H];
...


0 commentaires

3
votes

Le code dans cette question a changé un peu, invalidez les réponses correctes. Cette réponse s'applique à la 5ème version: comme le code tente actuellement de lire la mémoire ininitialisée, un optimiseur peut raisonnablement supposer que des choses inattendues se produisent.

De nombreuses étapes d'optimisation ont un modèle similaire: il existe un modèle d'instructions correspondant à l'état actuel de la compilation. Si le motif correspond à un moment donné, le motif correspondant est (paramétriquement) remplacé par une version plus efficace. Un exemple très simple d'un tel motif est la définition d'une variable qui n'est pas utilisée ultérieurement; Le remplacement dans ce cas est simplement une suppression.

Ces motifs sont conçus pour le code correct. Sur un code incorrect, les modèles peuvent simplement manquer de correspondre ou peuvent correspondre à des voies entièrement inattendues. Le premier cas ne conduit à aucune optimisation, le second cas peut entraîner des résultats totalement imprévisibles (certainement si le code modifié si optimisé)


0 commentaires