什么是最好的(最清洁、最高效的)的方式编写饱和外在C?

功能或宏应添加两个未签名的投入(既需要16和32位的版本),并返回各位-一(0xFFFF或0xFFFFFFFF)如果总和溢出。

目标是x86和手臂使用的海湾合作委员会(4.1.2)和Visual Studio(模拟,因此退回执行情况是确定的,那里)。

有帮助吗?

解决方案

您可能需要可移植的 C 代码,编译器将转换为适当的ARM程序集。 ARM有条件移动,这些可以以溢出为条件。然后算法变为add,并且如果检测到溢出,则有条件地将目标设置为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 -O3输出为adds32 :明显优于其他任何答案:

    add     edi, esi
    mov     eax, -1
    cmovae  eax, edi
    ret

其他提示

简单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中,您可能已经内置了饱和算术。 ARMv5 DSP扩展可以使寄存器饱和到任何位长。同样在ARM饱和度上通常很便宜,因为你可以有条件地执行大多数指令。

ARMv6甚至还有饱和的加法,减法和所有其他32位和打包数字的东西。

在x86上,您可以通过MMX或SSE获得饱和算术。

这一切都需要汇编程序,所以这不是你要求的。

也有C-tricks做饱和算术。这个小代码对dword的四个字节进行了饱和加法。它基于并行计算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无法在一个周期内加载所有可能的32位常量。

编辑:并行版本很可能比直接版本慢,但如果您一次必须使多个值饱和,它们会更快。

如果你关心的表现,你 真的 想要做这样的东西中单指令,其中x86有地饱和算术运算。

由于这种缺乏饱和算术标数学、一个可以获得情况在其业务上完成4-变量范围的单指令是 更多 于4倍,相当于C(和相应真正与8-变量范围的单指令):

sub8x8_dct8_c: 1332 clocks
sub8x8_dct8_mmx: 182 clocks
sub8x8_dct8_sse2: 127 clocks

零分支解决方案:

uint32_t sadd32(uint32_t a, uint32_t b)
{
    uint64_t s = (uint64_t)a+b;
    return -(s>>32) | (uint32_t)s;
}

一个好的编译器会优化它以避免做任何实际的64位算术( s&gt;&gt; 32 只是进位标志,而 - (s&gt;&gt; 32) sbb%eax,%eax 的结果。

在x86 asm(AT&amp; T语法, a b eax ebx 中,导致<代码> EAX ):

add %eax,%ebx
sbb %eax,%eax
or %ebx,%eax

8位和16位版本应该是显而易见的。签名版本可能需要更多工作。

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 */

编辑:现在您已经发布了自己的版本,我不确定我的版本是否更清晰/更好/效率更高/更合适。

我不确定这是否比Skizz的解决方案(总是配置文件)更快,但这里是一个替代的无分支组装解决方案。请注意,这需要条件移动(CMOV)指令,我不确定您的目标是否可用。


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

我们目前使用的实现是:

#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)))

最佳性能通常涉及内联汇编(正如一些人已经说过的那样)。

但对于便携式C,这些功能只涉及一次比较而没有任何类型转换(因此我认为是最优的):

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”的版本作为练习留给读者。 ; - )

我想,x86的最佳方法是使用内联汇编程序在添加后检查溢出标志。类似的东西:

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

它不是很便携,但恕我直言是最有效的方式。

分支免费x86 asm解决方案的替代方案是(AT&amp; T语法,eax和ebx中的a和b,导致eax):

add %eax,%ebx
sbb <*>,%ebx
//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:网页上使用的编辑器不允许我发布一个宏,即它不理解非缩进语法等!

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运算符( == != )和?:运算符。它只使用按位运算符和逻辑运算符。

饱和算术不是标准为C,但它往往是通过实施compiler内部函数,所以最有效的方法将不会是干净的。你必须添加 #ifdef 区块选择适当的方式。MSalters的回答是最快的x86架构。手臂你需要使用 __qadd16 功能(臂compiler)的 _arm_qadd16 (Visual Studio)对于16位版和 __qadd 32位的版本。他们会被自动翻译,一个手臂的指令。

链接:

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top