web-dev-qa-db-ja.com

Cで符号なしサチュレート加算を行う方法は?

Cでサチュレート加算を記述するための最良の(最もクリーンで最も効率的な)方法は何ですか?

関数またはマクロは、2つの符号なし入力(16ビットバージョンと32ビットバージョンの両方が必要)を追加し、合計がオーバーフローした場合はすべて1のビット(0xFFFFまたは0xFFFFFFFF)を返します。

ターゲットはx86であり、ARM gcc(4.1.2)とVisual Studioを使用しています(シミュレーションのみのため、フォールバック実装はそこで可能です)。

40
Frank Szczerba

おそらく、ここに移植可能な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  @
22
MSalters

プレーン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;} 

ほぼマクロ化されており、直接意味を伝えます。

24
Remo.D

条件付きジャンプのない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
}
18
Skizz

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ビット定数をロードできないため、サインマスク定数を変更する必要があります。

編集:パラレルバージョンは、単純な方法よりも低速である可能性が高いですが、一度に複数の値を飽和させる必要がある場合は高速です。

11

ゼロブランチソリューション:

_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およびbeaxおよび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
10
Dark Shikari
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 */

編集: あなたがあなたのバージョンを投稿したので、私が私がよりクリーン/より良い/より効率的/よりスタッドであるかどうかはわかりません。

7
DGentry

現在使用している実装は次のとおりです。

#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)))
3
Frank Szczerba

これがSkizzのソリューション(常にプロファイル)より速いかどうかはわかりませんが、分岐のない別のアセンブリソリューションを次に示します。これには条件付き移動(CMOV)命令が必要ですが、ターゲットで使用できるかどうかはわかりません。


uint32_t sadd32(uint32_t a, uint32_t b)
{
    __asm
    {
        movl eax, a
        addl eax, b
        movl edx, 0xffffffff
        cmovc eax, edx
    }
}
3
Adam Rosenfield

通常、最良のパフォーマンスにはインラインアセンブリが含まれます(既に述べたように)。

しかし、ポータブル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
Kevin

誰かが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);
 }

それは私が知っている最高の実装です

2
Hannodje

ブランチフリーのx86 asmソリューションの代替手段は次のとおりです(AT&T構文、eaxおよびebxのaおよびb、結果はeax):

add %eax,%ebx
sbb $0,%ebx
1
Ian Rogers

X86の最良の方法は、インラインアセンブラを使用して、追加後にオーバーフローフラグをチェックすることだと思います。何かのようなもの:

add eax, ebx
jno @@1
or eax, 0FFFFFFFFh
@@1:
.......

移植性はそれほど高くありませんが、IMHOが最も効率的な方法です。

1
Igor Semenov

飽和算術はCの標準ではありませんが、コンパイラ組み込み関数を介して実装されることが多いため、最も効率的な方法は最もクリーンではありません。適切な方法を選択するには、#ifdefブロックを追加する必要があります。 MSaltersの答えは、x86アーキテクチャーにとって最速です。 ARMの場合、__qadd16関数(ARMコンパイラ)を使用する必要があります。16ビットバージョンの場合は_arm_qadd16(Microsoft Visual Studio)、32ビットバージョンの場合は__qaddバージョン。これらは自動的に1つのARM命令に変換されます。

リンク:

0
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オペレーター(==!=) そしてその ?:演算子。それは単にビットごとの演算子と論理演算子を使用します。

0
Shangchih Huang
//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ページで使用されているエディターではマクロを投稿できません。つまり、インデントされていない構文などを理解できません。

0
twostickes

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に変換できます。 固定幅整数型 がシステムで使用できない場合があることにも注意してください。

0
0xbadf00d