52 votes

Pourquoi les compilateurs modernes ne coalescent-ils pas les accès mémoire voisins ?

Considérons le code suivant :

bool AllZeroes(const char buf[4])
{
    return buf[0] == 0 &&
           buf[1] == 0 &&
           buf[2] == 0 &&
           buf[3] == 0;
}

Assemblage de sortie de Clang 13 avec -O3 :

AllZeroes(char const*):                        # @AllZeroes(char const*)
        cmp     byte ptr [rdi], 0
        je      .LBB0_2
        xor     eax, eax
        ret
.LBB0_2:
        cmp     byte ptr [rdi + 1], 0
        je      .LBB0_4
        xor     eax, eax
        ret
.LBB0_4:
        cmp     byte ptr [rdi + 2], 0
        je      .LBB0_6
        xor     eax, eax
        ret
.LBB0_6:
        cmp     byte ptr [rdi + 3], 0
        sete    al
        ret

Chaque octet est comparé individuellement, mais cela aurait pu être optimisé en une seule comparaison d'int 32 bits :

bool AllZeroes(const char buf[4])
{
    return *(int*)buf == 0;
}

Résultant en :

AllZeroes2(char const*):                      # @AllZeroes2(char const*)
        cmp     dword ptr [rdi], 0
        sete    al
        ret

J'ai également vérifié GCC et MSVC, et aucun d'eux ne fait cette optimisation. Est-ce interdit par la spécification C++ ?

Editer : En changeant le AND court-circuité ( && ) à ET binaire ( & ) générera le code optimisé. De même, changer l'ordre de comparaison des octets n'affecte pas la génération du code : https://godbolt.org/z/Y7TcG93sP

65voto

anatolyg Points 8076

Si buf[0] est non nulle, le code n'accédera pas à buf[1] . La fonction doit donc retourner false sans vérifier les autres buf éléments. Si buf est proche de la fin de la dernière page de mémoire, buf[1] peut déclencher une faute d'accès. Le compilateur doit faire très attention à ne pas lire des choses dont la lecture peut être interdite.

22voto

hanshenrik Points 192

La première chose à comprendre est que f(const char buf[4]) ne garantit pas que le pointeur pointe sur 4 éléments, cela signifie exactement la même chose que const char *buf le 4 est complètement ignoré par le langage. (C99 a une solution à ce problème, mais elle n'est pas supportée par le C++, nous y reviendrons plus loin).

Étant donné que AllZeroes(memset(malloc(1),~0,1)) la mise en œuvre

bool AllZeroes(const char buf[4])
{
    return buf[0] == 0 &&
           buf[1] == 0 &&
           buf[2] == 0 &&
           buf[3] == 0;
}

devrait fonctionner, car elle n'essaie jamais de lire l'octet 2 (qui n'existe pas) lorsqu'elle remarque que l'octet 1 est différent de zéro, alors que l'implémentation

bool AllZeroes(const int32_t *buf)
{
    return (*buf == 0);
}

devrait se planter car il essaye de lire les 4 premiers octets alors qu'il n'en existe qu'un seul (1 octet malloqué seulement)

FWIW Clang le fait bien (et GCC ne le fait pas) en C99 avec l'implémentation suivante

_Bool AllZeroes(const char buf[static 4])
{
    return buf[0] == 0 &
           buf[1] == 0 &
           buf[2] == 0 &
           buf[3] == 0;
}

qui se compile de la même manière que

_Bool AllZeroes(const int32_t *buf)
{
    return (*buf == 0);
}

voir https://godbolt.org/z/Grqs3En3K (merci à Caze @libera #C pour avoir trouvé cela)

  • malheureusement buf[static 4], qui en C99 est une garantie du programmeur au compilateur que le pointeur pointe vers un minimum de 4 éléments, n'est pas supporté en C++.

14voto

Il y a le truc de l'évaluation des courts-circuits. Donc ça ne peut pas être optimisé comme vous le pensez. Si buf[0] == 0 est false buf[1] == 0 ne doit pas être vérifié. Il peut s'agir de l'UB ou d'un produit dont l'utilisation est interdite ou autre - tout cela doit continuer à fonctionner.

https://en.wikipedia.org/wiki/Short-circuit_evaluation

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