Cでサチュレート加算を記述するための最良の(最もクリーンで最も効率的な)方法は何ですか?
関数またはマクロは、2つの符号なし入力(16ビットバージョンと32ビットバージョンの両方が必要)を追加し、合計がオーバーフローした場合はすべて1のビット(0xFFFFまたは0xFFFFFFFF)を返します。
ターゲットはx86であり、ARM gcc(4.1.2)とVisual Studioを使用しています(シミュレーションのみのため、フォールバック実装はそこで可能です)。
おそらく、ここに移植可能なC
コードが必要です。これは、コンパイラが適切なARMアセンブリに変換します。ARMには条件付きの移動があり、これらは次に、アルゴリズムは追加になり、オーバーフローが検出された場合、宛先を条件付きでunsigned(-1)に設定します。
uint16_t add16(uint16_t a, uint16_t b)
{
uint16_t c = a + b;
if (c<a) /* Can only happen due to overflow */
c = -1;
return c;
}
これは、オーバーフローを検出するために別の計算に依存するのではなく、オーバーフローを修正するという点で他のアルゴリズムとは異なります。
x86-64 clang 3.7 -adds32のO3出力 :他のどの回答よりもはるかに優れています:
add edi, esi
mov eax, -1
cmovae eax, edi
ret
ARMv7:gcc 4.8 -O3 -mcpu=cortex-a15 -fverbose-asm
adds32の出力 :
adds r0, r0, r1 @ c, a, b
it cs
movcs r0, #-1 @ conditional-move
bx lr
16ビット:ARMの符号なし飽和加算命令(UADD16
)
add r1, r1, r0 @ tmp114, a
movw r3, #65535 @ tmp116,
uxth r1, r1 @ c, tmp114
cmp r0, r1 @ a, c
ite ls @
movls r0, r1 @,, c
movhi r0, r3 @,, tmp116
bx lr @
プレーンC:
uint16_t sadd16(uint16_t a, uint16_t b)
{ return (a > 0xFFFF - b) ? 0xFFFF : a + b; }
uint32_t sadd32(uint32_t a, uint32_t b)
{ return (a > 0xFFFFFFFF - b) ? 0xFFFFFFFF : a + b;}
ほぼマクロ化されており、直接意味を伝えます。
条件付きジャンプのないIA32の場合:
uint32_t sadd32(uint32_t a, uint32_t b)
{
#if defined IA32
__asm
{
mov eax,a
xor edx,edx
add eax,b
setnc dl
dec edx
or eax,edx
}
#Elif defined ARM
// ARM code
#else
// non-IA32/ARM way, copy from above
#endif
}
ARMでは、すでに飽和演算が組み込まれている可能性があります。ARMv5DSP拡張は、レジスタを任意のビット長に飽和させることができます。また、ARM条件付きでほとんどの命令を実行できるため、安価です。
ARMv6は、32ビットおよびパックされた数値に対して、飽和した加算、減算、およびその他すべてのものも備えています。
X86では、MMXまたはSSEを介して飽和演算が行われます。
これにはすべてアセンブラが必要なので、要求されたものとは異なります。
飽和演算を行うCトリックもあります。この小さなコードは、dwordの4バイトを飽和加算します。これは、32個の半加算器を並列で計算するという考えに基づいています。桁上げオーバーフローなしで数値を追加します。
これが最初に行われます。次にキャリーが計算され、加算され、加算がオーバーフローする場合はマスクに置き換えられます。
uint32_t SatAddUnsigned8(uint32_t x, uint32_t y)
{
uint32_t signmask = 0x80808080;
uint32_t t0 = (y ^ x) & signmask;
uint32_t t1 = (y & x) & signmask;
x &= ~signmask;
y &= ~signmask;
x += y;
t1 |= t0 & x;
t1 = (t1 << 1) - (t1 >> 7);
return (x ^ t0) | t1;
}
次のように、符号マスク定数と下部のシフトを変更することで、16ビット(または任意の種類のビットフィールド)で同じ結果を得ることができます。
uint32_t SatAddUnsigned16(uint32_t x, uint32_t y)
{
uint32_t signmask = 0x80008000;
uint32_t t0 = (y ^ x) & signmask;
uint32_t t1 = (y & x) & signmask;
x &= ~signmask;
y &= ~signmask;
x += y;
t1 |= t0 & x;
t1 = (t1 << 1) - (t1 >> 15);
return (x ^ t0) | t1;
}
uint32_t SatAddUnsigned32 (uint32_t x, uint32_t y)
{
uint32_t signmask = 0x80000000;
uint32_t t0 = (y ^ x) & signmask;
uint32_t t1 = (y & x) & signmask;
x &= ~signmask;
y &= ~signmask;
x += y;
t1 |= t0 & x;
t1 = (t1 << 1) - (t1 >> 31);
return (x ^ t0) | t1;
}
上記のコードは、16ビット値と32ビット値に対して同じことを行います。
関数が複数の値を並列に追加して飽和させる機能が必要ない場合は、必要なビットをマスクしてください。 ARMでは、ARMは1つのサイクルですべての可能な32ビット定数をロードできないため、サインマスク定数を変更する必要があります。
編集:パラレルバージョンは、単純な方法よりも低速である可能性が高いですが、一度に複数の値を飽和させる必要がある場合は高速です。
ゼロブランチソリューション:
_uint32_t sadd32(uint32_t a, uint32_t b)
{
uint64_t s = (uint64_t)a+b;
return -(s>>32) | (uint32_t)s;
}
_
優れたコンパイラはこれを最適化して、実際の64ビット演算を行わないようにします(_s>>32
_は単にキャリーフラグであり、-(s>>32)
は_sbb %eax,%eax
_の結果です)。
X86 asm(AT&T構文、a
およびb
のeax
およびebx
、結果はeax
):
_add %eax,%ebx
sbb %eax,%eax
or %ebx,%eax
_
8ビットと16ビットのバージョンは明らかです。署名されたバージョンでは、もう少し作業が必要になる場合があります。
パフォーマンスに関心がある場合、reallyはSIMDでこの種の処理を実行する必要があります。x86にはネイティブの飽和演算があります。
スカラー数学では飽和演算が欠如しているため、4変数全体のSIMDで実行される演算がmoreが同等のCの4倍より高速である(そして、8で対応する)場合があります。変数全体のSIMD):
sub8x8_dct8_c: 1332 clocks
sub8x8_dct8_mmx: 182 clocks
sub8x8_dct8_sse2: 127 clocks
uint32_t saturate_add32(uint32_t a, uint32_t b)
{
uint32_t sum = a + b;
if ((sum < a) || (sum < b))
return ~((uint32_t)0);
else
return sum;
} /* saturate_add32 */
uint16_t saturate_add16(uint16_t a, uint16_t b)
{
uint16_t sum = a + b;
if ((sum < a) || (sum < b))
return ~((uint16_t)0);
else
return sum;
} /* saturate_add16 */
編集: あなたがあなたのバージョンを投稿したので、私が私がよりクリーン/より良い/より効率的/よりスタッドであるかどうかはわかりません。
現在使用している実装は次のとおりです。
#define sadd16(a, b) (uint16_t)( ((uint32_t)(a)+(uint32_t)(b)) > 0xffff ? 0xffff : ((a)+(b)))
#define sadd32(a, b) (uint32_t)( ((uint64_t)(a)+(uint64_t)(b)) > 0xffffffff ? 0xffffffff : ((a)+(b)))
これがSkizzのソリューション(常にプロファイル)より速いかどうかはわかりませんが、分岐のない別のアセンブリソリューションを次に示します。これには条件付き移動(CMOV)命令が必要ですが、ターゲットで使用できるかどうかはわかりません。
uint32_t sadd32(uint32_t a, uint32_t b)
{
__asm
{
movl eax, a
addl eax, b
movl edx, 0xffffffff
cmovc eax, edx
}
}
通常、最良のパフォーマンスにはインラインアセンブリが含まれます(既に述べたように)。
しかし、ポータブルCの場合、これらの関数には1つの比較のみが含まれ、型キャストは含まれません(したがって、最適と考えています)。
unsigned saturate_add_uint(unsigned x, unsigned y)
{
if (y>UINT_MAX-x) return UINT_MAX;
return x+y;
}
unsigned short saturate_add_ushort(unsigned short x, unsigned short y)
{
if (y>USHRT_MAX-x) return USHRT_MAX;
return x+y;
}
マクロとして、次のようになります。
SATURATE_ADD_UINT(x, y) (((y)>UINT_MAX-(x)) ? UINT_MAX : ((x)+(y)))
SATURATE_ADD_USHORT(x, y) (((y)>SHRT_MAX-(x)) ? USHRT_MAX : ((x)+(y)))
読者への課題として、「unsigned long」と「unsigned long long」のバージョンを残しています。 ;-)
誰かが2の補数32ビット整数を使用して分岐せずに実装を知りたい場合に備えて。
警告!このコードでは、未定義の演算「-1だけ右シフト」を使用しているため、 Intel Pentium SAL命令 のプロパティを利用して、カウントオペランドを5ビットにマスクしています。
int32_t sadd(int32_t a, int32_t b){
int32_t sum = a+b;
int32_t overflow = ((a^sum)&(b^sum))>>31;
return (overflow<<31)^(sum>>overflow);
}
それは私が知っている最高の実装です
ブランチフリーのx86 asmソリューションの代替手段は次のとおりです(AT&T構文、eaxおよびebxのaおよびb、結果はeax):
add %eax,%ebx
sbb $0,%ebx
X86の最良の方法は、インラインアセンブラを使用して、追加後にオーバーフローフラグをチェックすることだと思います。何かのようなもの:
add eax, ebx
jno @@1
or eax, 0FFFFFFFFh
@@1:
.......
移植性はそれほど高くありませんが、IMHOが最も効率的な方法です。
飽和算術はCの標準ではありませんが、コンパイラ組み込み関数を介して実装されることが多いため、最も効率的な方法は最もクリーンではありません。適切な方法を選択するには、#ifdef
ブロックを追加する必要があります。 MSaltersの答えは、x86アーキテクチャーにとって最速です。 ARMの場合、__qadd16
関数(ARMコンパイラ)を使用する必要があります。16ビットバージョンの場合は_arm_qadd16
(Microsoft Visual Studio)、32ビットバージョンの場合は__qadd
バージョン。これらは自動的に1つのARM命令に変換されます。
リンク:
int saturating_add(int x, int y)
{
int w = sizeof(int) << 3;
int msb = 1 << (w-1);
int s = x + y;
int sign_x = msb & x;
int sign_y = msb & y;
int sign_s = msb & s;
int nflow = sign_x && sign_y && !sign_s;
int pflow = !sign_x && !sign_y && sign_s;
int nmask = (~!nflow + 1);
int pmask = (~!pflow + 1);
return (nmask & ((pmask & s) | (~pmask & ~msb))) | (~nmask & msb);
}
この実装は、制御フローを使用しません、campareオペレーター(==
、!=
) そしてその ?:
演算子。それは単にビットごとの演算子と論理演算子を使用します。
//function-like macro to add signed vals,
//then test for overlow and clamp to max if required
#define SATURATE_ADD(a,b,val) ( {\
if( (a>=0) && (b>=0) )\
{\
val = a + b;\
if (val < 0) {val=0x7fffffff;}\
}\
else if( (a<=0) && (b<=0) )\
{\
val = a + b;\
if (val > 0) {val=-1*0x7fffffff;}\
}\
else\
{\
val = a + b;\
}\
})
私は簡単なテストを行い、うまくいったようですが、まだ大げさではありません!これはSIGNED 32ビットで動作します。 op:Webページで使用されているエディターではマクロを投稿できません。つまり、インデントされていない構文などを理解できません。
C++を使用すると、Remo.Dのソリューションのより柔軟なバリアントを作成できます。
template<typename T>
T sadd(T first, T second)
{
static_assert(std::is_integral<T>::value, "sadd is not defined for non-integral types");
return first > std::numeric_limits<T>::max() - second ? std::numeric_limits<T>::max() : first + second;
}
これは、limits.h
で定義された制限を使用して、簡単にCに変換できます。 固定幅整数型 がシステムで使用できない場合があることにも注意してください。