web-dev-qa-db-ja.com

ビルトインなしでGCCにビッグエンディアンストアのbswap命令を生成させる方法は?

私は64ビット値をビッグエンディアン形式でメモリに格納する関数に取り組んでいます。リトルエンディアンとビッグエンディアンの両方のプラットフォームで動作するポータブルC99コードを記述し、最新のx86コンパイラにbswap命令を自動的に生成させることを望んでいました組み込みまたは組み込みなし。だから私は次の関数から始めました:

#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;
}

これは、この関数を次のようにコンパイルするclangで正常に機能します。

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

しかし、GCC バイトスワップの検出に失敗します 。私はいくつかの異なるアプローチを試みましたが、それらは事態を悪化させただけです。 GCCがビット単位のAND、シフト、ビット単位のORを使用してバイトスワップを検出できることを知っていますが、バイトを書き込むときに動作しないのはなぜですか?

編集:対応する GCCバグ を見つけました。

20
nwellnhof

これはトリックを行うようです:

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));
}

-O3付きのclang

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

-O3 -march=native付きのclang

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

-O3付きのgcc

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

-O3 -march=native付きのgcc

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

http://gcc.godbolt.org/ でclang 3.8.0およびgcc 5.3.0を使用してテストしました(そのため、下にあるプロセッサが正確にわかりません(-march=nativeの場合)しかし、私は最近のx86_64プロセッサを強く疑っています)


ビッグエンディアンアーキテクチャでも機能する関数が必要な場合は、 here からの回答を使用してシステムのエンディアンを検出し、ifを追加できます。ユニオンキャストバージョンとポインターキャストバージョンの両方が機能し、gccclangの両方によって最適化されるため、まったく同じAssembly(ブランチなし)。 godeboltの完全なコード

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));
}

インテル®64およびIA-32アーキテクチャーの命令セットリファレンス (3-542 Vol。2A):

MOVBE-バイトをスワップした後にデータを移動する

2番目のオペランド(ソースオペランド)からコピーされたデータに対してバイトスワップ操作を実行し、その結果を最初のオペランド(宛先オペランド)に格納します。 [...]

MOVBE命令は、メモリからの読み取り時またはメモリへの書き込み時にバイトをスワップするために提供されています。したがって、リトルエンディアン値をビッグエンディアン形式に、またはその逆に変換するためのサポートを提供します。

15
bolov

Godbolt Compiler Explorer のasm出力を使用したこの回答のすべての関数


GNU Cにはuint64_t __builtin_bswap64 (uint64_t x) があります。GNU C 4.3。なので、これは明らかにgcc/clangに、これに煩わされないコードを生成させる信頼できる方法

glibcは、マシンのエンディアンに応じて、htobe64htole64、および同様のホストとBEおよびLE関数との間でスワップを行うかどうかを提供します。 <endian.h> のドキュメントを参照してください。 manページには、バージョン2.9(2008年11月リリース)でglibcに追加されたと記載されています。

#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

-O1でもこれらの関数から適切なコードを安全に取得し、-marchがそれをサポートするCPUに設定されている場合はmovbeを使用しますinsn。


GNU Cをターゲットにしているが、glibcをターゲットにしていない場合は、glibcから定義を借用できます(ただし、LGPLされたコードであることを忘れないでください)。

#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__

GNU Cビルトインをサポートしていないコンパイラでうまくコンパイルできるフォールバックが本当に必要な場合は、@ bolovの回答のコードを使用して、適切にコンパイルされるbswapを実装できます。プリプロセッサマクロを使用して、スワップするかどうかを選択し( glibcが行うように )、Host-to-BE関数とHost-to-LE関数を実装できます。 glibcが使用する/-bswap when __builtin_bswapまたはx86 asmは利用できませんbolovが良かったとわかったマスクアンドシフトイディオムを使用します。


このエンディアンに依存しないコーディングのブログ投稿 からのコードは、gccを使用して bswapにコンパイルされますが、clang を使用するとnotになります。 IDKは、両方のパターン認識機能が認識するものがある場合。

// 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    $8, %rcx
    orq     %rax, %rcx
    ... completely naive implementation

この回答htonllは、shift/orと組み合わせて2つの32ビットbswapsにコンパイルされます。この種のはひどいですが、gccやclangのどちらでもひどいものではありません。


OPのコードのunion { uint64_t a; uint8_t b[8]; }バージョンには運がありませんでした。 clangはそれを64ビットbswapにコンパイルしますが、gccではさらに悪いコードにコンパイルすると思います。 (ゴッドボルトのリンクを参照してください)。

5
Peter Cordes

私はピーターの解決策が好きですが、これがHaswellで使用できる他のものです。 Haswellにはmovbe命令があります。これはそこで3 uops(_bswap r64_ +通常のロードまたはストアよりも安くはありません)ですが、Atom/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;
}
_

uint64_t tmp = load_bigend_u64(array[i]);のようなもので使用してください

これを逆にして_store_bigend_関数を作成するか、bswapを使用してレジスターの値を変更し、コンパイラーにロード/ストアさせます。


valueの配置が明確ではなかったため、vdestを返すように関数を変更します。

通常、機能はプリプロセッサマクロによって保護されます。 ___MOVBE___がmovbe機能フラグに使用されることを期待していますが、存在しません( このマシンには機能があります ):

_$ 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
jww