Comment optimiser les valeurs de retour de fonction en C et C ++ sur x86-64?

L’ ABI x86-64 spécifie deux registres de retour: rax et rdx , d’une rdx 64 bits (8 octets).

En supposant que x86-64 soit la seule plate-forme ciblée, laquelle de ces deux fonctions:

 uint64_t f(uint64_t * const secondReturnValue) { /* Calculate a and b. */ *secondReturnValue = b; return a; } std::pair g() { /* Calculate a and b, same as in f() above. */ return { a, b }; } 

donnerait de meilleures performances, étant donné l’état actuel des compilateurs C / C ++ ciblant x86-64? Y at-il des pièges de performance en utilisant l’une ou l’autre version? Les compilateurs (GCC, Clang) sont-ils toujours en mesure d’optimiser la std::pair à renvoyer dans rax et rdx ?

UPDATE: En règle générale, le renvoi d’une paire est plus rapide si le compilateur optimise les méthodes std::pair (exemples de sortie binary avec GCC 5.3.0 et Clang 3.8.0 ). Si f() n’est pas en ligne, le compilateur doit générer du code pour écrire une valeur en mémoire, par exemple:

 movq b, (%rdi) movq a, %rax retq 

Mais dans le cas de g() il suffit au compilateur de faire:

 movq a, %rax movq b, %rdx retq 

Étant donné que les instructions pour écrire des valeurs en mémoire sont généralement plus lentes que celles pour écrire des valeurs dans des registres, la deuxième version devrait être plus rapide.

    Comme l’ABI précise que, dans certains cas particuliers, deux registres doivent être utilisés pour obtenir le résultat à 2 mots, tout compilateur conforme devant obéir à cette règle.

    Cependant, pour de si petites fonctions, je suppose que la majeure partie de la performance proviendra de l’alignement.

    Vous voudrez peut-être comstackr et lier avec g++ -flto -O2 utilisant des optimisations en temps de liaison.

    Je suppose que la deuxième fonction (renvoyer une paire par deux registres) pourrait être légèrement plus rapide et que, dans certaines situations, le compilateur GCC pourrait s’aligner et optimiser la première dans la seconde.

    Mais vous devriez vraiment comparer si vous vous en souciez autant.

    Notez que l’ABI spécifie que toute petite structure doit être compressée dans des registres pour être transmise / renvoyée (si elle ne contient que des types entiers). Cela signifie que renvoyer un std::pair signifie que les valeurs doivent être décalées + OUr dans rax .

    C’est probablement encore mieux qu’un aller-retour en mémoire , car la création d’espace pour un pointeur et le passage de ce pointeur comme argument supplémentaire entraînent une certaine surcharge. (Autre que cela, cependant, un aller-retour à travers le cache L1 est assez bon marché, comme une latence de ~ 5c. Le magasin / chargement va presque certainement aller dans le cache L1, car la mémoire de stack est utilisée en permanence. Même s’il manque , le transfert de magasin peut toujours se produire, donc l’exécution ne s’arrête pas tant que le ROB ne s’est pas rempli, car le magasin ne peut pas se retirer (voir le guide sur la microarchie d’Agner Fog et d’autres choses à la page wiki x86 ).)

    Quoi qu’il en soit, voici le genre de code que vous obtenez avec gcc 5.3 -O2 , en utilisant des fonctions qui prennent des arguments au lieu de renvoyer des valeurs constantes au moment de la compilation (ce qui conduirait à movabs rax, 0x... ):

     #include  #include  #define type_t uint32_t type_t f(type_t * const secondReturnValue, type_t x) { *secondReturnValue = x+4; return x+2; } lea eax, [rsi+4] # LEA is an add-and-shift instruction that uses memory-operand syntax and encoding mov DWORD PTR [rdi], eax lea eax, [rsi+2] ret std::pair g(type_t x) { return {x+2, x+4}; } lea eax, [rdi+4] lea edx, [rdi+2] sal rax, 32 or rax, rdx ret type_t use_pair(std::pair pair) { return pair.second + pair.first; } mov rax, rdi shr rax, 32 add eax, edi ret 

    Donc c’est vraiment pas mal du tout. Deux ou trois insins dans l’appelant et appelé pour emballer et décompresser une paire de valeurs uint32_t . Nulle part près de retourner une paire de valeurs uint64_t , cependant.

    Si vous optimisez spécifiquement pour x86-64 et tenez compte de ce qui se passe pour les fonctions non en ligne avec plusieurs valeurs de retour, préférez renvoyer std::pair (ou int64_t , évidemment), même si vous affectez ces paires pour réduire les entiers dans l’appelant. Notez que dans l’ABI x32 ( -mx32 ), les pointeurs n’ont que -mx32 . Ne supposez pas que les pointeurs sont 64 bits lors de l’optimisation pour x86-64, si vous vous souciez de cet ABI.

    Si l’un des membres de la paire est 64 bits, ils utilisent des registres distincts . Cela ne fait rien de stupide comme de séparer une valeur entre la moitié haute d’un registre et la moitié basse d’une autre.