Comment faire pour que GCC génère des instructions bswap pour big endian store sans builtins?

je travaille sur une fonction qui stocke une valeur 64 bits en mémoire au format big endian. J'espérais que je pourrais écrire portable C99 code qui fonctionne à la fois peu et big endian plates-formes et que les compilateurs x86 modernes génèrent un bswap instruction automatiquement sans aucun builtins ou intrinsèques. J'ai donc commencé avec la fonction suivante:

#include <stdint.h>

void
encode_bigend_u64(uint64_t value, void *vdest) {
    uint64_t bigend;
    uint8_t *bytes = (uint8_t*)&bigend;
    bytes[0] = value >> 56;
    bytes[1] = value >> 48;
    bytes[2] = value >> 40;
    bytes[3] = value >> 32;
    bytes[4] = value >> 24;
    bytes[5] = value >> 16;
    bytes[6] = value >> 8;
    bytes[7] = value;
    uint64_t *dest = (uint64_t*)vdest;
    *dest = bigend;
}

Cela fonctionne très bien pour clang qui compile cette fonction à:

bswapq  %rdi
movq    %rdi, (%rsi)
retq

mais GCC ne détecte pas l'échange des octets. J'ai essayé un couple de différentes approches, mais ils ont fait qu'aggraver les choses. Je sais que GCC peut détecter les swaps byte en utilisant bitwise-and, shift, et bitwise-or, mais pourquoi ne fonctionne-t-il pas en écrivant des octets?

Edit: j'ai trouvé leGCC bug.

17
demandé sur nwellnhof 2016-04-08 13:41:57

3 réponses

Ce qui semble faire l'affaire:

void encode_bigend_u64(uint64_t value, void* dest)
{
  value =
      ((value & 0xFF00000000000000u) >> 56u) |
      ((value & 0x00FF000000000000u) >> 40u) |
      ((value & 0x0000FF0000000000u) >> 24u) |
      ((value & 0x000000FF00000000u) >>  8u) |
      ((value & 0x00000000FF000000u) <<  8u) |      
      ((value & 0x0000000000FF0000u) << 24u) |
      ((value & 0x000000000000FF00u) << 40u) |
      ((value & 0x00000000000000FFu) << 56u);
  memcpy(dest, &value, sizeof(uint64_t));
}

clang -O3

encode_bigend_u64(unsigned long, void*):
        bswapq  %rdi
        movq    %rdi, (%rsi)
        retq

clang -O3 -march=native

encode_bigend_u64(unsigned long, void*):
        movbeq  %rdi, (%rsi)
        retq

gcc avec -O3

encode_bigend_u64(unsigned long, void*):
        bswap   %rdi
        movq    %rdi, (%rsi)
        ret

gcc avec -O3 -march=native

encode_bigend_u64(unsigned long, void*):
        movbe   %rdi, (%rsi)
        ret

Testé avec clang 3.8.0 et gcc 5.3.0 sur http://gcc.godbolt.org/ (donc je ne sais pas exactement quel processeur est en dessous (pour le -march=native) mais je soupçonne fortement un x86_64 récent processeur)


si vous voulez une fonction qui fonctionne aussi pour les grandes architectures endian, vous pouvez utiliser les réponses de ici pour détecter le boutisme du système et ajouter un if. Les deux versions fonctionnent et sont optimisées par les deux gcc et clang résultant en la exactement la même assemblée (pas de succursales). code complet sur godebolt!--28-->:

int is_big_endian(void)
{
    union {
        uint32_t i;
        char c[4];
    } bint = {0x01020304};

    return bint.c[0] == 1;
}

void encode_bigend_u64_union(uint64_t value, void* dest)
{
  if (!is_big_endian())
    //...
  memcpy(dest, &value, sizeof(uint64_t));
}

Intel® 64 et IA-32 Architectures de l'Instruction de Référence (3-542 Vol. 2A):

MOVBE-Déplacer des Données Après la Permutation des Octets

effectue une opération d'échange d'octets sur les données copiées à partir de la seconde opérande (opérande source) et stocker le résultat dans le premier opérande (opérande de destination). [...]

l'instruction MOVBE est prévue pour échanger les octets sur une lecture de mémoire ou sur une écriture à mémoire; fournissant ainsi un soutien pour conversion des valeurs de little-endian au format big-endian et vice versa.

13
répondu bolov 2017-05-23 12:09:37

toutes les fonctions de cette réponse avec sortie asm sur le Godbolt Compilateur Explorer


GNU C a un uint64_t __builtin_bswap64 (uint64_t x), depuis GNU C 4.3. c'est apparemment la façon la plus fiable d'obtenir gcc / clang pour générer du code qui ne craint pas pour cela.

glibc fournit htobe64,htole64, et hôte similaire aux fonctions BE et LE qui changent ou non, en fonction de l'encanteur de la machine. Voir la docs pour <endian.h>. La page de manuel dit qu'ils ont été ajoutés à glibc dans la version 2.9 (sortie 2008-11).

#define _BSD_SOURCE             /* See feature_test_macros(7) */

#include <stdint.h>

#include <endian.h>
// ideal code with clang from 3.0 onwards, probably earlier
// ideal code with gcc from 4.4.7 onwards, probably earlier
uint64_t load_be64_endian_h(const uint64_t *be_src) { return be64toh(*be_src); }
    movq    (%rdi), %rax
    bswap   %rax

void store_be64_endian_h(uint64_t *be_dst, uint64_t data) { *be_dst = htobe64(data); }
    bswap   %rsi
    movq    %rsi, (%rdi)

// check that the compiler understands the data movement and optimizes away a double-conversion (which inline-asm `bswap` wouldn't)
// it does optimize away with gcc 4.9.3 and later, but not with gcc 4.9.0 (2x bswap)
// optimizes away with clang 3.7.0 and later, but not clang 3.6 or earlier (2x bswap)
uint64_t double_convert(uint64_t data) {
  uint64_t tmp;
  store_be64_endian_h(&tmp, data);
  return load_be64_endian_h(&tmp);
}
    movq    %rdi, %rax

vous obtenez en toute sécurité un bon code même à -O1 à partir de ces fonctions, et ils utilisent movbe quand -march est défini à un CPU qui supporte cet insn.


si vous ciblez GNU C, mais pas glibc, vous pouvez emprunter la définition de glibc (rappelez-vous que c'est du code LGPLed):

#ifdef __GNUC__
# if __GNUC_PREREQ (4, 3)

static __inline unsigned int
__bswap_32 (unsigned int __bsx) { return __builtin_bswap32 (__bsx);  }

# elif __GNUC__ >= 2
    // ... some fallback stuff you only need if you're using an ancient gcc version, using inline asm for non-compile-time-constant args
# endif  // gcc version
#endif // __GNUC__

Si vous si vous avez vraiment besoin d'un repli qui pourrait bien se compiler sur les compilateurs qui ne supportent pas les builtins GNU C, le code de la réponse de @bolov pourrait être utilisé pour implémenter un bswap qui se compile bien. Les macros pré-processeur peuvent être utilisées pour choisir d'échanger ou non ( comme glibc fait), pour implémenter les fonctions host-to-BE Et host-to-LE. bswap utilisé par la glibc quand __builtin_bswap ou x86 asm n'est pas disponible utilise le masque-et-maj idiome que bolov trouvé était bon. le CCG le reconnaît mieux que de changer.


le code de c'Endian-agnostique codage blog compile bswap avec gcc, mais avec clang. IDK s'il y a quelque chose que les deux reconnaîtront.

// Note that this is a load, not a store like the code in the question.
uint64_t be64_to_host(unsigned char* data) {
    return
      ((uint64_t)data[7]<<0)  | ((uint64_t)data[6]<<8 ) |
      ((uint64_t)data[5]<<16) | ((uint64_t)data[4]<<24) |
      ((uint64_t)data[3]<<32) | ((uint64_t)data[2]<<40) |
      ((uint64_t)data[1]<<48) | ((uint64_t)data[0]<<56);
}

    ## gcc 5.3 -O3 -march=haswell
    movbe   (%rdi), %rax
    ret

    ## clang 3.8 -O3 -march=haswell
    movzbl  7(%rdi), %eax
    movzbl  6(%rdi), %ecx
    shlq    , %rcx
    orq     %rax, %rcx
    ... completely naive implementation

htonllcette réponse compile de deux 32bit bswap s combiné avec shift / or. Ça craint, mais c'est pas terrible avec soit gcc ou clang.


je n'ai pas de chance avec un union { uint64_t a; uint8_t b[8]; } version du code de L'OP. clang le compile toujours à un 64bit bswap, mais je pense qu'il compile encore plus de code avec gcc. (Voir le lien godbolt).

5
répondu Peter Cordes 2017-05-23 11:54:17

J'aime la solution de Peter, mais voici quelque chose d'autre que vous pouvez utiliser sur Haswell. Haswell a le movbe instruction, qui est de 3 uops là (pas moins cher que bswap r64 + une charge normale ou magasin), mais est plus rapide sur l'Atome / Silvermont (https://agner.org/optimize/):

// AT&T syntax, compile without -masm=intel
inline
uint64_t load_bigend_u64(uint64_t value)
{
    __asm__ ("movbe %[src], %[dst]"   // x86-64 only
             :  [dst] "=r" (value)
             :  [src] "m" (value)
            );
    return value;
}

l'Utiliser avec quelque chose comme uint64_t tmp = load_bigend_u64(array[i]);

vous pouvez inverser ceci pour faire un store_bigend fonction, or use bswap pour modifier une valeur dans un registre et laisser le compilateur load/store il.


j'ai modifier la fonction de retour value parce que l'alignement de vdest n'était pas clair pour moi.

habituellement, une fonctionnalité est gardée par une macro préprocesseur. Je m'attends à __MOVBE__ à utiliser pour le movbe drapeau de fonctionnalité, mais il n'est pas présent (cette machine a la fonction):

$ gcc -march=native -dM -E - < /dev/null | sort
...
#define __LWP__ 1
#define __LZCNT__ 1
#define __MMX__ 1
#define __MWAITX__ 1
#define __NO_INLINE__ 1
#define __ORDER_BIG_ENDIAN__ 4321
...
2
répondu jww 2018-09-02 23:52:47