2つの符号なしバイトb
とx
があるとします。 bsub
をb - x
として、badd
を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);
これを行うためのより良い方法がありますか、つまり、いくつかのハッキーなビット操作がありますか?
記事 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;
}
簡単な方法は、以下のようにオーバーフローを検出し、それに応じて値をリセットすることです
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秒でした。このソリューションも読みやすくなっています。
減算の場合:
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);
Gccまたはclangの最近の十分なバージョン(他にもあるかもしれません)を使用している場合、 built-ins を使用してオーバーフローを検出できます。
if (__builtin_add_overflow(a,b,&c))
{
c = UINT_MAX;
}
アセンブリまたは組み込み関数を使用する場合は、最適なソリューションがあると思います。
減算の場合:
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
。否定carry_flag
は0を生成するため、!carry_flag * result = 0
オーバーフローがある場合。それ以来 0 - 1
は符号なし整数値をその最大値に設定します。キャリーがない場合、関数は加算の結果を返し、キャリーがある場合、選択した整数値の最大値を返します。
さらに:
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);
比較演算子や乗算は必要ありません。
Boost Library Incubator で安全な数値ライブラリを使用することもできます。 int、longなどのドロップイン置換を提供します。これにより、検出されないオーバーフロー、アンダーフローなどが発生しないことが保証されます。
すべて符号なしバイト演算で実行できます
// Addition without overflow
return (b > 255 - a) ? 255 : a + b
// Subtraction without underflow
return (b > a) ? 0 : a - b;
2バイトでこれを行う場合は、可能な限り単純なコードを使用します。
200億バイトでこれを実行したい場合は、プロセッサーで使用可能なベクター命令とそれらを使用できるかどうかを確認してください。プロセッサは、1つの命令でこれらの操作の32を実行できることがわかります。
これはどうですか:
bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;
bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;
これらのメソッドを頻繁に呼び出す場合、最速の方法はビット操作ではなく、おそらくルックアップテーブルです。操作ごとに長さ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];
使い方?さて、符号なし文字のすべての可能な減算を事前に計算したいと思います。結果は-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のキャッシュに完全にロードされ、計算がさらに高速化されます。
追加でも同じように機能しますが、少し異なるテーブル(最初の256要素がインデックスになり、最後の255要素が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ビットの計算は少し速くなると思います。あなた次第