• [^] # Re: Ai-je bien compris ?

    Posté par (site web personnel) . En réponse au journal Performances des processeurs Intel et optimisation. Évalué à 10.

    Le benchmark ne teste donc pas la validité de l'implémentation (c'est testé par des tests unitaires dédiés), mais seulement sa rapidité, en mono-thread. Cela permet donc d'évaluer l'overhead des instructions atomiques, qui est ici négatif, ce qui est surprenant.

    Déjà ton code est faux. var est volatile et tu le passes en tant que non volatile dans les deux fonctions. GCC emet un warning et ce n'est pas pour rien, cela n'a pas de sens, ce bench n'a pas de sens.

    Et en supprimant le volatile ?

    Je m'explique, le qualifier volatile en c/c++ n'est utile que pour des mappings hardware (genre device mapppé, ou interruption, …), mais il ne change RIEN pour l'atomicité des opérations, mais il empêche le compilateur d'optimiser certains traitements.

    Hors le atomic est une opération hardware spécifique du CPU (Je ne sais pas comment elle fonctionne, mais il y a des chances que ton CPU soit capable en un unique cycle de faire le get/l'incrementation/le set).

    A ce moment, le compilateur va simplement remplacer ton __sync par l'appel à l'instruction du CPU qui va bien.

    De l'autre coté, le compilateur va être forcé de générer le code pour récupérer la valeur, l’incrémenter et la remettre en mémoire.

    En gros, vire volatile et regarde ce qui ce passe. Volatile n'est pas nécessaire ni suffisant pour faire des opération thread-safe et se contente simplement d’empêcher des optimisation du compilateur ou du CPU.

    On va creuser un peu. les fonctions thread_safe et not_thread_safe en -Os donnent:

    thread_safe:
    lock addl 1,ドル (%rdi)
    ret
    not_thread_safe:
    addl 1,ドル (%rdi)
    ret

    Bon, rien de magique. Maintenant la boucle, bon là c'est marrant, mais la boucle genere moins d'assembleur en unsafe que en safe (globalement GCC se rend compte que c'est une boucle, l'inline, se rend compte que tu fais N fois l'incrementation et remplace tout par une unique incrementation de N, contrairement à la version /safe/ qui force la boucle et l'appel de N fois la primitive atomique.) Bref, tant que on a pas ton code complet, cela n'a pas de sens.

    Bon, par contre si on met volatile AUSSI sur les prototypes des fonctions:

    void thread_safe(volatile int *ptr)
    {
     __sync_fetch_and_add(ptr, 1);
    }
    void not_thread_safe(volatile int *ptr)
    {
     *ptr += 1;
    }
    #define BENCHMARK_ITERATIONS 10
    int benchmark(const int c)
    {
     volatile int var = 0;
     for (unsigned int i=0; i<c; ++i)
     {
     not_thread_safe(&var);
     }
     return var;
    }
    
    

    (J'ai un peu changé la fonction benchmark pour que GCC ne puisse pas inliner la boucle aussi facilement qu'avant)
    Là cela devient drole, car le code (en O3) du milieu de la boucle en unsafe est donc:

    .L7: # debut de boucle
     movl -4(%rsp), %edx # Fetch memoire (à cause du volatile)
     addl 1,ドル %eax # compteur de boucle++
     addl 1,ドル %edx # valeur++
     cmpl %edi, %eax # test de boucle
     movl %edx, -4(%rsp) # store memoire (à cause du volatile)
     jne .L7 # Jump en fonction du test de boucle
    
    

    Contre en safe:

    .L7: # debut de boucle
     lock addl 1,ドル -4(%rsp) # Incrementation atomique (note le lock)
     addl 1,ドル %eax # compteur de boucle
     cmpl %edi, %eax # comparaison
     jne .L7 # Jump en fonction du test de boucle
    
    

    Donc la raison ici c'est que le volatile force le fetch/store alors que l'atomique ne fait rien… Test sans le volatile.