符号なしバイトの飽和減算/加算


83

私は2つの符号なしバイトを持っている想像bしてxbsubasb - xbaddasを計算する必要がありb + xます。ただし、これらの操作中にアンダーフロー/オーバーフローが発生することは望ましくありません。例(擬似コード):

b = 3; x = 5;
bsub = b - x; // bsub must be 0, not 254

そして

b = 250; x = 10;
badd = b + x; // badd must be 255, not 4

これを行うための明白な方法には、分岐が含まれます。

bsub = b - min(b, x);
badd = b + min(255 - b, x);

これを行うためのより良い方法、つまりハッキーなビット操作による方法があるかどうか疑問に思います。


13
y ^ ((x ^ y) & -(x < y))以下のためintの型を評価min(x, y)分岐せず。これは、これまでの状況に基づいて、最終的な解決策の一部を形成する可能性があります。
バトシェバ2015年


8
これはCまたはC ++の質問ですか?一つ選んでください。
fuz 2015年

9
@AlanCampbellそれはSaturatingArithmeticと呼ばれます。
Shafik Yaghmour 2015年

7
ポータブルにする必要がありますか?特定のアーキテクチャを検討している場合は、おそらく1つの優れた命令があるからです。ARMにはバイトの飽和ベクトルの加算と減算があることを私は知っています。X86では、_mm_adds_epi8組み込み関数は1つの命令で16バイトの飽和加算を実行します。
porglezomp 2015年

回答:


86

Branchfree Saturating Arithmeticの記事は、このための戦略を提供します。

それらの加算ソリューションは次のとおりです。

u32b sat_addu32b(u32b x, u32b y)
{
    u32b res = x + y;
    res |= -(res < x);

    return res;
}

uint8_t用に変更:

uint8_t  sat_addu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x + y;
    res |= -(res < x);

    return res;
}

そしてそれらの減算ソリューションは次のとおりです。

u32b sat_subu32b(u32b x, u32b y)
{
    u32b res = x - y;
    res &= -(res <= x);

    return res;
}

uint8_t用に変更:

uint8_t sat_subu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x - y;
    res &= -(res <= x);

    return res;
}

2
@ user1969104の場合もありますが、記事のコメントが示すように、単項マイナスを適用する前にunsignedにキャストすることで解決されます。実際には、2の補数以外のものを処理する必要はほとんどありません
Shafik Yaghmour 2015年

2
これは良いCの答えかもしれませんが、あまり良いC ++の答えではありません。
Yakk-Adam Nevraumont 2015年

4
@Yakkこれが「悪い」C ++の答えである理由は何ですか?これらは基本的な数学演算であり、Cのみ、または悪いC ++としてどのように解釈されるかわかりません。
JPhi1618 2015年

4
@ JPhi1618より良いC ++の答えはtemplate<class T>struct sat{T t;};、飽和するオーバーロードされた演算子を使用することかもしれません。名前空間の適切な使用。主に砂糖。
Yakk-Adam Nevraumont 2015年

6
@ヤック、ああ、わかりました。私はこれを、OPが必要に応じて適応できる最小限の例として見ました。私はそれが完全な実装になるとは思っていません。明確にしていただきありがとうございます。
JPhi1618 2015年

40

簡単な方法は、オーバーフローを検出し、それに応じて以下のように値をリセットすることです。

bsub = b - x;
if (bsub > b)
{
    bsub = 0;
}

badd = b + x;
if (badd < b)
{
    badd = 255;
}

GCCは、-O2を使用してコンパイルするときに、オーバーフローチェックを条件付き割り当てに最適化できます。

他のソリューションと比較して、最適化の程度を測定しました。私のPCで1000000000以上の操作を行った場合、このソリューションと@ShafikYaghmourのソリューションの平均は4.2秒、@ chuxのソリューションの平均は4.8秒でした。このソリューションも読みやすくなっています。


5
@ user694733離れて最適化されるのではなく、キャリーフラグに応じて条件付き割り当てに最適化されます。
fuz 2015年

2
はいuser694733は正しいです。条件付き割り当てに最適化されます。
user1969104 2015年

これは、すべての場合に機能するとは限りません。たとえば、badd:b = 155 x = 201、badd = 156よりも大きく、bよりも大きくなります。操作に応じて、結果を2つの変数のmin()またはmax()と比較する必要があります
Cristian F

@CristianF 155 + 201 = 156をどのように計算しますか?155 + 201 = 356%256 = 100である必要があると思います。b、x値の任意の組み合わせで、min()、max()が必要になるとは思いません。
user1969104 2015年

16

減算の場合:

diff = (a - b)*(a >= b);

添加:

sum = (a + b) | -(a > (255 - b))

進化

// sum = (a + b)*(a <= (255-b)); this fails
// sum = (a + b) | -(a <= (255 - b)) falis too

@R_Kappに感謝します

おかげで @NathanOliverにます

この演習では、単純にコーディングすることの価値を示します。

sum = b + min(255 - b, a);

sumおそらく(a + b) | -(a <= (255 - b))
R_Kapp 2015年

と仮定して行うことはできますが、これは非常に複雑に見えるため、(頭痛以外に)何かを得ることができるかどうかはわかりません。sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFFsizeof(int) > sizeof(unsigned char)
user694733 2015年

@ user694733はい、そして多分(a+b+1)*(a <= (255-b)) - 1
chux -復活モニカ

@NathanOliver見落としに感謝します-これの明らかな側面はsub、制限があったので簡単だったということ0です。しかし、他の制限は複雑さをもたらし、user2079303のコメントに従います。
chux -復活モニカ

1
@ user1969104 OPは、「より良い」(コードスペースと速度のパフォーマンス)、ターゲットプラットフォーム、コンパイラについて明確ではありませんでした。速度評価は、投稿されていないより大きな問題のコンテキストで最も理にかなっています。
chux -復活モニカ

13

gccまたはclangの最新バージョン(おそらく他のバージョンも)を使用している場合は、組み込みを使用してオーバーフローを検出できます。

if (__builtin_add_overflow(a,b,&c))
{
  c = UINT_MAX;
}

これが最良の答えです。ビットマジックの代わりに組み込みのコンパイラを使用すると、高速になるだけでなく、より明確になり、コードの保守が容易になります。
頭足類2015年

ありがとう、@ erebos。私は間違いなくそれが利用可能なプラットフォームでこれを試してみます。
ovk 2015年

3
これでgccにブランチレスコードを生成させることができません。これは少し残念です。ここで特に不幸なことは、clangがこれらに異なる名前を使用していることです。
Shafik Yaghmour 2015年

1
@Cephalopodそしてそれは完全に非クロスプラットフォームであり、おそらく別のコンパイラでも動作しません。21世紀には良い解決策ではありません。
ela782 2015年

1
@ Ela782それはまったく逆です。組み込みは、20世紀には良い解決策ではありません。未来へようこそ!
頭足類2015年

3

追加のために:

unsigned temp = a+b;  // temp>>8 will be 1 if overflow else 0
unsigned char c = temp | -(temp >> 8);

減算の場合:

unsigned temp = a-b;  // temp>>8 will be 0xFF if neg-overflow else 0
unsigned char c = temp & ~(temp >> 8);

比較演算子や乗算は必要ありません。


3

アセンブリまたは組み込み関数を使用する場合は、最適なソリューションがあると思います。

減算の場合:

命令を使用できますsbb

MSVCでは、組み込み関数_subborrow_u64(他のビットサイズでも使用可能)を使用できます。

使用方法は次のとおりです。

// *c = a - (b + borrow)
// borrow_flag is set to 1 if (a < (b + borrow))
borrow_flag = _subborrow_u64(borrow_flag, a, b, c);

これが私たちがあなたの状況にそれを適用する方法です

uint64_t sub_no_underflow(uint64_t a, uint64_t b){
    uint64_t result;
    borrow_flag = _subborrow_u64(0, a, b, &result);
    return result * !borrow_flag;
}

追加のために:

命令を使用できますadcx

MSVCでは、組み込み関数_addcarry_u64(他のビットサイズでも使用可能)を使用できます。

使用方法は次のとおりです。

// *c = a + b + carry
// carry_flag is set to 1 if there is a carry bit
carry_flag = _addcarry_u64(carry_flag, a, b, c);

これが私たちがあなたの状況にそれを適用する方法です

uint64_t add_no_overflow(uint64_t a, uint64_t b){
    uint64_t result;
    carry_flag = _addcarry_u64(0, a, b, &result);
    return !carry_flag * result - carry_flag;
}

これは引き算ほど好きではありませんが、かなり気の利いたものだと思います。

追加がオーバーフローした場合、carry_flag = 1。Not-ingcarry_flagは0を生成するため!carry_flag * result = 0、オーバーフローがある場合。また0 - 1、符号なし積分値を最大値に設定するため、キャリーがない場合は加算の結果を返し、キャリーがある場合は選択した積分値の最大値を返します。


1
この回答は特定の命令セットアーキテクチャ(x86?)に対するものであり、ターゲットアーキテクチャ(SPARC、MIPS、ARMなど)ごとに再実装する必要があることをお伝えしておきます
TobySpeight19年

2

これはどうですか:

bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;

bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;

(明らかな?)タイプミスを修正しましたが、それでもこれは正しいとは思いません。
バトシェバ2015年

これには分岐も含まれます。
fuz 2015年

三項演算子とif / elseステートメントの違いは何ですか?最適化せずにアセンブリの簡単な質問だけでこの回答を削除しますか?

@GRC違いはありません。
fuz 2015年

@GRC FUZxxlは正しいですが、いつものように、自分で試してみてください。組み立てがわからない場合でも(不明な点がある場合は、SOで質​​問できます)、長さや手順を確認するだけでわかります。
edmz 2015年

2

すべて符号なしバイト演算で実行できます

// Addition without overflow
return (b > 255 - a) ? 255 : a + b

// Subtraction without underflow
return (b > a) ? 0 : a - b;

1
これは実際には最良の解決策の1つです。以前に減算または加算を行った他のすべてのユーザーは、実際にはC ++で未定義の動作を作成しているため、コンパイラーは必要な処理を実行できます。実際には、ほとんどの場合、何が起こるかを予測できますが、それでもなおです。
エイドリアンハメリン2015年

2

これを2バイトで実行する場合は、可能な限り単純なコードを使用してください。

200億バイトでこれを実行する場合は、プロセッサで使用可能なベクトル命令と、それらを使用できるかどうかを確認してください。プロセッサが1つの命令でこれらの操作のうち32を実行できることに気付くかもしれません。


2

Boost LibraryIncubatorで安全な数値ライブラリを使用することもできます。これは、int、longなどのドロップイン置換を提供します。これにより、検出されないオーバーフロー、アンダーフローなどが発生することはありません。


7
ライブラリの使用方法の例を提供すると、これがより良い答えになります。さらに、それらはブランチレス保証を提供しますか?
Shafik Yaghmour 2015年

ライブラリには、広範なドキュメントと例があります。しかし、結局のところ、適切なヘッダーを含め、intをsafe <int>に置き換えるのと同じくらい簡単です。
ロバートラミー2015年

ブランチレス?私はあなたが枝のない男だと思います。ライブラリはテンプレートメタプログラミングを使用して、必要な場合にのみ実行時チェックを含めます。たとえば、unsignedcharとunsignedcharを掛けると、unsignedintになります。これはオーバーフローすることはないため、チェックを行う必要はまったくありません。一方、unsigned time unsignedはオーバーフローする可能性があるため、実行時にチェックする必要があります。
ロバートラミー2015年

1

これらのメソッドを頻繁に呼び出す場合、最速の方法はビット操作ではなく、おそらくルックアップテーブルです。操作ごとに長さ511の配列を定義します。マイナス(減算)の例

static unsigned char   maxTable[511];
memset(maxTable, 0, 255);           // If smaller, emulates cutoff at zero
maxTable[255]=0;                    // If equal     - return zero
for (int i=0; i<256; i++)
    maxTable[255+i] = i;            // If greater   - return the difference

配列は静的であり、一度だけ初期化されます。これで、減算をインラインメソッドとして、またはプリコンパイラを使用して定義できます。

#define MINUS(A,B)    maxTable[A-B+255];

使い方?さて、unsignedcharsのすべての可能な減算を事前に計算したいと思います。結果は-255から+255まで変化し、合計511の異なる結果になります。考えられるすべての結果の配列を定義しますが、Cでは負のインデックスからアクセスできないため、+ 255([A-B + 255])を使用します。配列の中心へのポインターを定義することにより、このアクションを削除できます。

const unsigned char *result = maxTable+255;
#define MINUS(A,B)    result[A-B];

次のように使用します。

bsub  = MINUS(13,15); // i.e 13-15 with zero cutoff as requested

実行は非常に高速であることに注意してください。結果を得るために1つの減算と1つのポインタの違いだけ。分岐なし。静的配列は非常に短いため、計算をさらに高速化するためにCPUのキャッシュに完全にロードされます

同じことが追加でも機能しますが、テーブルが少し異なります(255を超えるカットオフをエミュレートするために、最初の256要素がインデックスになり、最後の255要素が255に等しくなります。

ビット演算を主張する場合、(a> b)を使用する答えは間違っています。これはまだ分岐として実装される可能性があります。符号ビット技術を使用する

// (num1>num2) ? 1 : 0
#define        is_int_biggerNotEqual( num1,num2) ((((__int32)((num2)-(num1)))&0x80000000)>>31)

これで、減算と加算の計算に使用できます。

分岐せずに関数max()、min()をエミュレートする場合は、次のようにします。

inline __int32 MIN_INT(__int32 x, __int32 y){   __int32 d=x-y; return y+(d&(d>>31)); }              

inline __int32 MAX_INT(__int32 x, __int32 y){   __int32 d=x-y; return x-(d&(d>>31)); }

上記の私の例では、32ビット整数を使用しています。これは64に変更できますが、32ビットの計算は少し速く実行されると思います。あなた次第


2
実際にはそうはならないでしょう。まず、もちろん、テーブルのロードは遅いです。ビット演算には1サイクルかかり、メモリからのロードには約80nsかかります。L1キャッシュからでも、20 nsの範囲にあります。これは、3GHzCPUではほぼ7サイクルです。
edmz 2015年

あなたは完全に正しいわけではありません。LUTメソッドはいくつかのサイクルを必要としますが、ビット操作も単一のサイクルではありません。いくつかの順次アクションがあります。たとえば、MAX()の計算のみで、2つの減算、論理演算、および1つの右シフトが必要です。そして、整数の昇格/降格を忘れないでください
DanielHsH 2015年

1
当然、レジスタオペランドを想定して、単一のビット演算には1サイクルかかるということです。Shafikが示したコードを使用して、clangは4つの基本命令を出力します。また(x > y)、ブランチレスです。
edmz 2015年

まず、(x> y)は分岐を使用する場合があります。実行しているアーキテクチャがわかりません。Intelアーキテクチャではブランチレスである可能性があることに同意する傾向があります。ほとんどのスマートフォンはIntelではありません。それが、組み立て説明書がいくつあるかわからない理由でもあります。PCで私のソリューションを試してください。結果を聞いてみたいです。
DanielHsH 2015年

1
L1キャッシュは20nsよりもはるかに高速で、おそらく4プロセッササイクルのオーダーです。そして、おそらく未使用の実行ユニットを使用し、とにかく完全にパイプライン化されます。それを測定します。また、20nsは3 GHzCPUで60サイクルです。
gnasher729 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.