ビット演算は予期しない変数サイズになります


24

環境

PICマイクロコントローラー用の8ビットCコンパイラーを使用して最初にコンパイルされたCコードを移植しています。符号なしグローバル変数(たとえば、エラーカウンター)がゼロにロールオーバーしないようにするために使用された一般的なイディオムは次のとおりです。

if(~counter) counter++;

ここでのビット演算子はすべてのビットを反転し、ステートメントが真であるのcounterは、最大値よりも小さい場合のみです。重要なことに、これは変数のサイズに関係なく機能します。

問題

現在、GCCを使用する32ビットARMプロセッサをターゲットにしています。同じコードが異なる結果を生成することに気づきました。私たちが知る限りでは、ビットごとの補数演算は予想とは異なるサイズの値を返すようです。これを再現するには、GCCでコンパイルします。

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

出力の最初の行で、期待どおりの結果が得られますi。1バイトです。ただし、のビット単位の補数iは実際には4バイトであり、これとの比較では期待される結果が得られないため、問題が発生します。たとえば、次の場合(i適切に初期化されたはどこですかuint8_t):

if(~i) i++;

i0xFFから0x00までの「折り返し」が表示されます。この動作は、以前のコンパイラと8ビットPICマイクロコントローラーで意図したとおりに機能していた場合と比較して、GCCで異なります。

次のようにキャストすることでこれを解決できることを認識しています。

if((uint8_t)~i) i++;

または、

if(i < 0xFF) i++;

ただし、どちらの回避策でも、変数のサイズは既知である必要があり、ソフトウェア開発者にとってエラーが発生しやすくなります。これらの種類の上限チェックは、コードベース全体で行われます。そこの変数の複数のサイズがある(例えば、uint16_tおよびunsigned charなど)、そうでなければ作業コードベースでこれらを変更すると、私たちは楽しみにしているものではありません。

質問

問題についての私たちの理解は正しいですか、そしてこのイディオムを使用した各ケースを再訪する必要のない、これを解決するために利用可能なオプションはありますか?私たちの仮定は正しいですか?ビット単位の補数のような演算は、オペランドと同じサイズの結果を返す必要がありますか?プロセッサーのアーキテクチャーによっては、これが壊れるようです。クレイジーな薬を飲んでいるような気がします。Cはこれよりも移植性が少し高いはずです。繰り返しますが、これについての私たちの理解は間違っている可能性があります。

表面的にはこれは大きな問題のようには見えないかもしれませんが、以前は機能していたこのイディオムは何百もの場所で使用されており、費用のかかる変更を進める前にこれを理解したいと思っています。


注:ここには一見似ているが正確には重複していない質問があります。charのビット単位の演算では32ビットの結果が得られます

そこで議論された問題の実際の要点はわかりませんでした。つまり、ビット単位の補数の結果のサイズは、オペレーターに渡されたものとは異なります。


14
「ビットワイズ補数のような演算は、オペランドと同じサイズの結果を返すべきであるという私たちの仮定は正しいですか?」いいえ、これは正しくありません。整数のプロモーションが適用されます。
Thomas Jager

2
これらは問題の解決策を提供していないため、確かに関連はありますが、これらがこの特定の質問の重複であるとは確信していません。
コーディグレイ

3
クレイジーな薬を飲んでいるような気がします。Cはこれよりも移植性が少し高いはずです。8ビット型で整数の昇格が得られなかった場合、コンパイラはC標準互換ではありませんでした。その場合、すべての計算を実行してチェックし、必要に応じて修正する必要があると思います。
user694733

1
本当に重要でないカウンターとは別に、「十分なスペースがあればそれをインクリメントし、それ以外は忘れる」ことができるロジックを疑問に思っているのは私だけですか?コードを移植する場合、uint_8の代わりにint(4バイト)を使用できますか?それは多くの場合あなたの問題を防ぐでしょう。
パック

1
@puckそのとおりです。4バイトに変更できますが、既存のシステムと通信するときに互換性が失われます。意図はエラーが発生したことを知ることであり、したがって1バイトのカウンタで十分であり、そのままです。
チャーリーソルト

回答:


26

あなたが見ているのは、整数昇格の結果です。式で整数値が使用されるほとんどの場合、値のタイプが値よりも小さい場合int、に昇格されintます。これは、C標準のセクション6.3.1.1p2に記載されています

以下は、式で使用することができるどこintunsigned intに使用することができます

  • 整数変換ランクがand のランク以下である整数型(intまたは以外unsigned int)のオブジェクトまたは式。intunsigned int
  • 型のビットフィールド_Boolint ,signed int、, orunsigned int`。

intが元のタイプのすべての値を表すことができる場合(ビットフィールドの幅によって制限されている場合)、値はに変換されintます。それ以外の場合は、に変換されます unsigned int。これらは整数昇格と呼ばれます。他のすべてのタイプは、整数の昇格によって変更されません。

したがって、変数の型uint8_tと値が255の場合、キャストまたは代入以外の演算子を使用するとint、操作を実行する前に、まず値が255の型に変換されます。これがsizeof(~i)、1ではなく4を与える理由です。

セクション6.5.3.3では、整数の昇格が~演算子に適用されると説明しています。

~演算子の結果は、その(昇格された)オペランドのビット単位の補数です(つまり、変換されたオペランドの対応するビットが設定されていない場合にのみ、結果の各ビットが設定されます)。整数の昇格はオペランドで実行され、結果は昇格された型になります。プロモートされた型が符号なしの型である場合、式はその型で表現可能な~E最大値からマイナスされたものと同等Eです。

したがって、32ビットを想定し、8ビットの値があるint場合、32ビットの値に変換され、それに適用すると、次のようになります。counter0xff0x000000ff~0xffffff00

おそらく、これを処理する最も簡単な方法は、型を知らなくても、インクリメント後に値が0かどうかを確認し、そうであればデクリメントすることです。

if (!++counter) counter--;

符号なし整数のラップアラウンドは両方向で機能するため、値0をデクリメントすると正の最大値が得られます。


1
if (!++counter) --counter;プログラマーによっては、コンマ演算子を使用するよりも奇妙な場合があります。
Eric Postpischil

1
別の選択肢は++counter; counter -= !counter;です。
Eric Postpischil

@EricPostpischil実際には、私はあなたの最初のオプションの方が好きです。編集。
dbush

15
これは、どのように書いても醜く、判読できません。あなたはこのようなイディオムを使用する必要がある場合は、すべてのメンテナンスプログラマに好意を行うと、インライン関数として、それを包む:ようなものincrement_unsigned_without_wraparoundかをincrement_with_saturation。個人的には、汎用の3オペランドclamp関数を使用します。
コーディグレイ

5
また、引数の型によって動作が異なるため、これを関数にすることはできません。タイプジェネリックマクロを使用する必要があります。
user2357112は

7

中にはsizeof(I); 変数iのサイズを要求するので、1

中にはsizeof(〜I)。あなたはあなたのケースでは、int型である式のタイプのサイズを要求します4


使用する

if(〜i)

(uint8_tを使用した場合)が255の値でないかどうかを確認するには、あまり読みにくいです。

if (i != 255)

ポータブルで読みやすいコードができます


変数には複数のサイズがあります(たとえば、uint16_tやunsigned charなど)。

あらゆるサイズの符号なしを管理するには:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

式は定数なので、コンパイル時に計算されます。

書式#include <limits.hに>のためのCHAR_BITする#include <stdint.h>のためのuintmax_t


3
質問には、処理するサイズが複数あることが明示されているため、!= 255不十分です。
Eric Postpischil

@EricPostpischilああ、そうです、忘れたので、 "if(i!=((1u << sizeof(i)* 8)-1))"は常に符号なしと想定していますか?
ブルーノ

1
unsignedオブジェクトの全幅のシフトはC標準で定義されていないため、これはオブジェクトでは定義されませんが、で修正できます(2u << sizeof(i)*CHAR_BIT-1) - 1
Eric Postpischil

ああそう、ofc、CHAR_BIT、私の悪い
ブルーノ

2
より広いタイプの安全のために、1つを使用するかもしれません((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1
Eric Postpischil

5

いくつかの符号なし整数型のx場合、「1を追加するが、表現可能な最大値でクランプする」ためのオプションがいくつかありますx

  1. x型で表現可能な最大値よりも小さい場合にのみ1を追加します。

    x += x < Maximum(x);

    の定義については、次の項目を参照してくださいMaximum。この方法は、比較、ある種の条件付きセットまたは移動、追加などの効率的な命令に対してコンパイラーによって最適化される可能性が高くなります。

  2. タイプの最大値と比較します。

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (この計算2 NNはビット数であるxことにより、2をシフトすることによって、N -1ビット。我々は、代わりに1シフトこれを行うためのN型のビット数によってシフトがCによって定義されていないため、ビット標準。CHAR_BITマクロは一部に馴染みがない場合があります。これは、1バイトのビット数であるため、sizeof x * CHAR_BITのビット数であり、型のビット数も同じですx。)

    これは、美観と明快さのために必要に応じてマクロでラップできます。

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. xを使用して、ゼロにラップする場合はインクリメントして修正しますif

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. x式を使用して、ゼロにラップする場合はインクリメントして修正します。

    ++x; x -= !x;

    これは名目上はブランチレスです(パフォーマンスに有利な場合もあります)が、コンパイラは上記と同じように実装できます。必要に応じてブランチを使用しますが、ターゲットアーキテクチャに適切な命令がある場合は無条件命令を使用する可能性があります。

  5. 上記のマクロを使用したブランチレスオプションは次のとおりです。

    x += 1 - x/Maximum(x);

    xがそのタイプの最大値である場合、これはと評価されx += 1-1ます。それ以外の場合はx += 1-0です。ただし、分割は多くのアーキテクチャでやや遅くなります。コンパイラは、コンパイラとターゲットアーキテクチャによっては、除算のない命令にこれを最適化する場合があります。


1
マクロの使用を推奨する回答に賛成票を投じることはできません。Cにはインライン関数があります。インライン関数内では簡単に実行できないマクロ定義内では何も実行していません。マクロを使用する場合は、明確にするために戦略的に括弧で囲んでください。演算子<<の優先順位は非常に低くなっています。Clangはこれについて警告し-Wshift-op-parenthesesます。良いニュースは、最適化コンパイラがされていないあなたはそれが遅いことを心配する必要はありませんので、ここでの分裂を生成する予定。
コーディグレイ

1
@CodyGray、関数でこれを実行できると思われる場合は、答えを書いてください。
Carsten S

2
@CodyGray:sizeof xC関数内に実装することはできませんx。これは、いくつかの固定型のパラメーター(または他の式)でなければならないためです。呼び出し元が使用する引数タイプのサイズを生成できませんでした。マクロはできます。
Eric Postpischil

2

stdint.hの前は、変数のサイズはコンパイラによって異なり、Cの実際の変数の型は引き続きint、longなどであり、そのサイズに関してコンパイラの作成者によって定義されています。いくつかの標準的またはターゲット固有の仮定ではありません。次に、作成者はstdint.hを作成して2つの世界をマップする必要があります。これが、stdint.hがuint_thisをint、long、shortにマップする目的です。

別のコンパイラからコードを移植していて、char、short、int、longを使用している場合は、各型を調べて自分で移植する必要があります。これを回避する方法はありません。そして、変数の適切なサイズになるか、宣言は変更されますが、記述されたコードは機能します。

if(~counter) counter++;

または...マスクまたはタイプキャストを直接指定します

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

結局のところ、このコードを機能させたい場合は、新しいプラットフォームに移植する必要があります。どのようにあなたの選択。はい、各ケースにヒットする時間を費やして正しく実行する必要があります。そうしないと、さらにコストがかかるこのコードに戻り続けることになります。

移植する前にコード上の変数の型と変数の型のサイズを分離する場合は、これを行う変数を分離し(grepが容易である必要があります)、stdint.h定義を使用して宣言を変更します。びっくりするかもしれませんが、間違ったヘッダーが使用されることがあるので、チェックを入れて、夜によく眠れるようにします

if(sizeof(uint_8)!=1) return(FAIL);

そして、そのコーディングスタイルは機能しますが(if(〜counter)counter ++;)、現在および将来の移植性の要望から、マスクを使用してサイズを具体的に制限すること(および宣言に依存しないこと)が最善です。コードは最初に書かれるか、単に移植を終えれば、あとで改めて移植する必要はありません。または、コードを読みやすくするために、x <0xFF thenまたはx!= 0xFFまたはそのようなものを実行すると、コンパイラーはこれらのソリューションの場合と同じコードにコードを最適化できます。 ...

製品の重要性や、パッチ/アップデートを送信したり、トラックを転がしたり、実験室に歩いて何回で問題を解決したりするかによって異なります。100かそれ以下の場合は、それほど大きなポートではありません。


0
6.5.3.3単項算術演算子
...
4 演算子の結果は、~その(昇格された)オペランドのビット単位の補数です(つまり、変換されたオペランドの対応するビットが設定されていない場合にのみ、結果の各ビットが設定されます。 )。整数の昇格はオペランドで実行され、結果は昇格されたタイプになります。プロモートされた型が符号なしの型である場合、式はその型で表現可能な~E最大値からマイナスされたものと同等Eです。

C 2011オンラインドラフト

問題は、演算子が適用される前にのオペランド~が昇格intされることです。

残念ながら、これを解決する簡単な方法はないと思います。書き込み

if ( counter + 1 ) counter++;

プロモーションもそこで適用されるため、役に立ちません。私が提案できる唯一のことは、そのオブジェクトに表現させたい最大に対していくつかのシンボリック定数を作成し、それに対してテストすることです。

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;

整数昇格のポイントに感謝します-これは私たちが直面している問題のようです。ただし、指摘する価値があるのは、2番目のコードサンプルでは-1、カウンターが254(0xFE)で安定するため、これは必要ないということです。いずれにせよ、このアプローチは、私の質問で述べたように、このイディオムに参加するコードベースの変数サイズが異なるため、理想的ではありません。
チャーリーソルト
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.