2バイトを符号付き16ビット整数に変換する正しい方法は何ですか?


31

では、この答えzwolはこの主張をしました。

2バイトのデータを外部ソースから16ビットの符号付き整数に変換する正しい方法は、次のようなヘルパー関数を使用することです。

#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 8) | 
                   (((uint32_t)data[1]) << 0);
    return ((int32_t) val) - 0x10000u;
}

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 0) | 
                   (((uint32_t)data[1]) << 8);
    return ((int32_t) val) - 0x10000u;
}

上記の関数のどちらが適切かは、配列にリトルエンディアン表現またはビッグエンディアン表現が含まれるかどうかによって異なります。エンディアンネスはここでの問題ではありません、なぜzwolがに変換さ0x10000uれたuint32_t値から減算するのか疑問に思っていint32_tます。

なぜこれが正しい方法ですか?

戻り値の型に変換するときに、実装で定義された動作をどのように回避しますか?

2の補数表現を想定できるので、この単純なキャストはどのように失敗しますか。 return (uint16_t)val;

この素朴なソリューションの何が問題になっていますか:

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}

へのキャスト時の正確な動作int16_tは実装によって定義されるため、単純なアプローチは移植できません。
nwellnhof

@nwellnhofキャストはありませんint16_t
MM

タイトルの質問は、使用するマッピングを指定しないと答えられません
MM

4
どちらのアプローチも、実装で定義された動作に依存します(符号なしの値を、値を表現できない符号付きの型に変換します)。例えば。最初のアプローチで0xFFFF0001uint16_t0xFFFFuとして表現できません。2番目のアプローチでは、として表現できませんint16_t
Sander De Dycker

1
「2の補数表現を想定できるため」[引用が必要]。C89とC99は1の補数と符号の大きさの表現を否定しませんでした。Qv、stackoverflow.com
Eric Towers

回答:


20

Ifはint16ビットである中での発現の値ならば、あなたのバージョンでは、処理系定義の動作に依存しているreturn文がの範囲外ですint16_t

ただし、最初のバージョンにも同様の問題があります。たとえば、int32_tがtypedef for intで、入力バイトが両方0xFFの場合、returnステートメントでの減算の結果は、UINT_MAXに変換されint16_tたときに実装定義の動作になります。

私があなたがリンクする答えにいくつかの主要な問題があります。


2
しかし、正しい方法は何ですか?
idmean

@idmean質問に回答する前に説明が必要です。質問の下のコメントでリクエストしましたが、OPは応答しませんでした
MM

1
@MM:私はエンディアンネスが問題ではないことを指定して質問を編集しました。私がzwolが解決しようとしている問題は、宛先タイプに変換するときの実装定義の動作ですが、私はあなたに同意します。彼の方法には他の問題があるため、彼は間違っていると思います。どのように実装定義の動作を効率的に解決しますか?
chqrlie

@chqrlieforyellowblockquotesエンディアンについては特に言及していません。2つの入力オクテットの正確なビットを単にint16_t
MM

@MM:はい、それはまさに問題です。私はバイトを書きましたが、タイプがそうであるように、正しい単語は確かにオクテットでなければなりませんuchar8_t
chqrlie

7

通常の2の補数ではなく、符号ビットまたは1の補数表現を使用するプラットフォームでもこれは問題なく正しく、機能するはずです。入力バイトは2の補数であると見なされます。

int le16_to_cpu_signed(const uint8_t data[static 2]) {
    unsigned value = data[0] | ((unsigned)data[1] << 8);
    if (value & 0x8000)
        return -(int)(~value) - 1;
    else
        return value;
}

ブランチのため、他のオプションよりも高価になります。

これにより、表現がプラットフォームでの表現にどのようにint関連するかについての想定が回避さunsignedれます。キャスト先intは、ターゲットタイプに適合する任意の数値の算術値を保持するために必要です。反転により、16ビットの数値の最上位ビットが確実にゼロになるため、値が適合します。次に、単項-と1の減算により、2の補数否定の通常の規則が適用されます。プラットフォームによっては、ターゲットINT16_MINintタイプに適合しない場合でもオーバーフローする可能性がlongあります。その場合は使用する必要があります。

問題の元のバージョンとの違いは、返品時に発生します。オリジナルは常に減算され0x10000、2の補数は符号付きオーバーフローでint16_t範囲にラップさせますが、このバージョンは明示的ifに符号付きラップオーバーを回避します(これは未定義です)。

現在、実際に使用されているほとんどすべてのプラットフォームで、2の補数表現が使用されています。実際、プラットフォームがstdint.hを定義する標準に準拠している場合、2の補数を使用int32_tする必要があります。このアプローチが役立つ場合があるのは、整数データ型をまったく持たない一部のスクリプト言語の場合です。上記の浮動小数点演算を変更すると、正しい結果が得られます。


C標準ではint16_t、特にintxx_t、その符号なしのバリアントは、ビットを埋め込まずに2の補数表現を使用する必要があることを義務付けています。これらのタイプをホストしint、の別の表現を使用するには、意図的にひねくれたアーキテクチャが必要になりますが、DS9Kをこのように構成できると思います。
chqrlie

@chqrlieforyellowblockquotes良い点、int混乱を避けるために使用するように変更しました。実際、プラットフォームで定義されint32_tている場合、2の補数でなければなりません。
jpa

これらの型は、C99で次のように標準化されました。C99 7.18.1.1正確な幅の整数型 typedef名はintN_t 、width N、パディングビットなし、2の補数表現の符号付き整数型を指定します。したがって、int8_t幅がちょうど8ビットの符号付き整数型を示します。他の表現も標準でサポートされていますが、他の整数型用です。
chqrlie

更新されたバージョンで(int)valueは、型intが16ビットしかない場合の動作が実装で定義されています。を使用する必要があると思いますが(long)value - 0x10000、2以外の補数アーキテクチャでは、値0x8000 - 0x10000を16ビットとして表すことができないintため、問題は解決しません。
chqrlie

@chqrlieforyellowblockquotesええ、同じことに気づきましたlong。代わりに〜で修正しましたが、同じようにうまく機能します。
jpa

6

別の方法-使用union

union B2I16
{
   int16_t i;
   byte    b[2];
};

プログラム内:

...
B2I16 conv;

conv.b[0] = first_byte;
conv.b[1] = second_byte;
int16_t result = conv.i;

first_byteそしてsecond_byteほとんど、またはビッグエンディアンモデルに応じて交換することができます。この方法は良くありませんが、代替手段の1つです。


2
unionタイプのパンニングは不特定の動作ではありませんか?
Maxim Egorushkin

1
@MaximEgorushkin:ウィキペディアは、C標準を解釈するための信頼できる情報源ではありません。
Eric Postpischil

2
@EricPostpischilメッセージではなくメッセンジャーに焦点を合わせるのは賢明ではありません。
Maxim Egorushkin

1
@MaximEgorushkin:ああ、そうだね。同じサイズであると仮定するbyte[2]int16_t、2つの可能な順序付けのどちらか一方であり、ビット単位の任意の値を入れ替えたものではありません。したがって、少なくともコンパイル時に、実装のエンディアンを検出できます。
Peter Cordes

1
規格では、共用体メンバーの値はメンバーに格納されているビットをその型の値表現として解釈した結果であると明確に規定されています。タイプの表現が実装定義であるinsofarasに実装定義された側面があります。
MM

6

算術演算子shiftbitwise-or in expression (uint16_t)data[0] | ((uint16_t)data[1] << 8)int、より小さいタイプでは機能しないため、これらのuint16_t値はint(またはunsignedifにsizeof(uint16_t) == sizeof(int))昇格されます。それでも、値が含まれるのは下位2バイトだけなので、これで正しい答えが得られるはずです。

ビッグエンディアンからリトルエンディアンへの変換(リトルエンディアンのCPUを想定)のための別の正確なバージョン:

#include <string.h>
#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    memcpy(&r, data, sizeof r);
    return __builtin_bswap16(r);
}

memcpyの表現をコピーするために使用されますint16_t。これは、これを行うための標準に準拠した方法です。このバージョンも1つの命令movbeにコンパイルされます。アセンブリを参照してください。


1
@MM 1つの理由__builtin_bswap16は、ISO Cでのバイトスワップを効率的に実装できないためです。
Maxim Egorushkin

1
違います; コンパイラーは、コードがバイトスワッピングを実装していることを検出し、それを効率的な組み込みとして変換できます
MM

1
変換int16_tuint16_t明確に定義されている:負の値は、より大きい値に変換INT_MAXするが、背中にこれらの値を変換するuint16_t:実装定義の動作で6.3.1.3符号付きおよび符号なし整数 整数型の値を別の整数型の他than_Boolに変換されるとすれば、1。値は新しいタイプで表すことができ、変更されません。... 3.そうでない場合、新しいタイプは署名され、値をそのタイプで表すことができません。結果は実装定義であるか、実装定義の信号が発生します。
chqrlie

1
@MaximEgorushkin gccは16ビットバージョンではそれほどうまくいかないようですが、clangはntohs/ __builtin_bswapおよび|/ <<パターンに対して同じコードを生成します。gcc.godbolt.org
j87

3
@MM:マキシムは「現在のコンパイラでは実際にはできない」と言っていると思います。もちろん、コンパイラーは一度だけ吸うことができず、連続したバイトを整数にロードすることを認識できませんでした。GCC7または8は、GCC3が数十年前にバイトリバースを破棄した後、バイトリバース不要な場合のロード/ストア合体を最終的に再導入しました。しかし、一般的にコンパイラーは、実際にはCPUが効率的に実行できるが、ISO Cが移植可能に公開することを無視/拒否した多くのことについて助けを必要とする傾向があります。ポータブルISO Cは、効​​率的なコードビット/バイト操作に適した言語ではありません。
Peter Cordes

4

これは、移植可能で明確な動作のみに依存する別のバージョンです(ヘッダー#include <endian.h>は標準ではなく、コードは次のとおりです)。

#include <endian.h>
#include <stdint.h>
#include <string.h>

static inline void swap(uint8_t* a, uint8_t* b) {
    uint8_t t = *a;
    *a = *b;
    *b = t;
}
static inline void reverse(uint8_t* data, int data_len) {
    for(int i = 0, j = data_len / 2; i < j; ++i)
        swap(data + i, data + data_len - 1 - i);
}

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
#if __BYTE_ORDER == __LITTLE_ENDIAN
    uint8_t data2[sizeof r];
    memcpy(data2, data, sizeof data2);
    reverse(data2, sizeof data2);
    memcpy(&r, data2, sizeof r);
#else
    memcpy(&r, data, sizeof r);
#endif
    return r;
}

リトルエンディアンバージョンのコンパイルシングルにmovbeして、命令clanggccバージョン少ない最適な、参照アセンブリ


あなたの主な懸念があったように思わ@chqrlieforyellowblockquotes uint16_tint16_t変換、このバージョンはので、ここであなたが行く、その変換はありません。
Maxim Egorushkin

2

すべての貢献者の回答に感謝したいと思います。共同作業の要約は次のとおりです。

  1. C標準7.20.1.1のとおり、Exact-width integer types:types uint8_tint16_tおよびuint16_tパディングビットなしの2の補数表現を使用する必要があるため、表現の実際のビットは、配列内の2バイトのビットであり、関数名。
  2. (unsigned)data[0] | ((unsigned)data[1] << 8)(リトルエンディアンバージョンの)を使用して符号なし16ビット値を計算すると、単一の命令にコンパイルされ、符号なし16ビット値が生成されます。
  3. C標準6.3.1.3に従い、符号付きおよび符号なし整数:型の値を符号付きの型uint16_tに変換すると、値がint16_t宛先の型の範囲にない場合、動作が定義された動作になります。表現が正確に定義されている型には、特別な規定はありません。
  4. この実装定義の動作を回避するには、符号なしの値がより大きいかどうかをテストしINT_MAX、を減算することで対応する符号付きの値を計算します0x10000zwolによって提案されているようにすべての値に対してこれを行うint16_tと、同じ実装定義の動作で範囲外の値が生成される可能性があります。
  5. 0x8000ビットをテストすると、コンパイラーは非効率的なコードを生成します。
  6. 実装で定義された動作なしのより効率的な変換では、ユニオンを介した型パンニングを使用しますが、このアプローチの定義についての議論は、C標準の委員会レベルでさえも未解決のままです。
  7. 型パンニングは、移植性があり、定義された動作を使用して実行できますmemcpy

ポイント2と7を組み合わせると、gccclangの両方を使用して単一の命令に効率的にコンパイルされる、移植可能な完全に定義されたソリューションがあります。

#include <stdint.h>
#include <string.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    uint16_t u = (unsigned)data[1] | ((unsigned)data[0] << 8);
    memcpy(&r, &u, sizeof r);
    return r;
}

int16_t le16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    uint16_t u = (unsigned)data[0] | ((unsigned)data[1] << 8);
    memcpy(&r, &u, sizeof r);
    return r;
}

64ビットアセンブリ

be16_to_cpu_signed(unsigned char const*):
        movbe   ax, WORD PTR [rdi]
        ret
le16_to_cpu_signed(unsigned char const*):
        movzx   eax, WORD PTR [rdi]
        ret

私は言語弁護士ではありませんが、char他のタイプのオブジェクト表現をエイリアス化したり、オブジェクト表現を含めることができるのはタイプだけです。uint16_tcharタイプの1つではないためmemcpyuint16_tto int16_tは明確に定義された動作ではありません。標準ではchar[sizeof(T)] -> T > char[sizeof(T)]memcpy明確に定義された変換のみが必要です。
Maxim Egorushkin

memcpyof uint16_tto int16_tは、せいぜい実装定義であり、移植性がなく、明確に定義されていないため、一方から他方への割り当てとまったく同じであり、魔法のようにmemcpyuint16_t2の補数表現を使用するかどうか、またはパディングビットが存在するかどうかは問題ではありません。これは、C標準で定義または要求されている動作ではありません。
Maxim Egorushkin

非常に多くの言葉で、あなたの「解決策」は置き換えr = uて要約しますmemcpy(&r, &u, sizeof u)が、後者は前者よりも優れていませんね?
Maxim Egorushkin
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.