在C ++中进行基本128位整数计算的有效方法?

时间:2014-12-02 23:59:06

标签: c++ assembly x86 intel-edison int128

几年前我需要一种方法用Cuda做一些基本的128位整数运算: 128 bit integer on cuda?。 现在我遇到了同样的问题,但这次我需要在32位嵌入式系统(英特尔爱迪生)上运行一些基本的128位算术(总和,位移和乘法),它不支持任何类型的128位。但是,直接支持64位整数(unsigned long long int)。

我天真地尝试使用上次在CPU上回答的asm代码,但是我遇到了一堆错误。我对asm真的没有经验,所以:有64位整数的最有效方法是在128位中实现加法,乘法和位移?

2 个答案:

答案 0 :(得分:4)

更新:由于OP尚未接受答案< hint>< hint>,我附加了更多代码。

使用上面讨论的库可能是一个好主意。虽然您今天可能只需要一些功能,但最终您可能会发现还需要一个功能。然后又一次。直到最终你最终编写,调试和维护自己的128位数学库。这是浪费你的时间和精力。

那就是说。如果您决定自己动手:

1)你之前问过的cuda问题已经有了乘法的c代码。它有问题吗?

2)转移可能不会从使用asm中受益,所以c解决方案对我来说也是有意义的。 虽然如果性能确实是一个问题,我会看看Edison是否支持SHLD / SHRD,可能使这一点更快。否则,m 也许这样的方法?

my_uint128_t lshift_uint128 (const my_uint128_t a, int b)
{
   my_uint128_t res;
   if (b < 32) {    
      res.x = a.x << b;
      res.y = (a.y << b) | (a.x >> (32 - b));
      res.z = (a.z << b) | (a.y >> (32 - b));
      res.w = (a.w << b) | (a.z >> (32 - b));
   } elseif (b < 64) {
      ...
   }

   return res;
}

更新:由于Edison可能支持SHLD / SHRD,因此可能的替代方案比上面的“c”代码更高效。与声称速度更快的所有代码一样,您应该对其进行测试。

inline
unsigned int __shld(unsigned int into, unsigned int from, unsigned int c)
{
   unsigned int res;

   if (__builtin_constant_p(into) &&
       __builtin_constant_p(from) &&
       __builtin_constant_p(c))
   {
      res = (into << c) | (from >> (32 - c));
   }
   else
   {
      asm("shld %b3, %2, %0"
          : "=rm" (res)
          : "0" (into), "r" (from), "ic" (c)
          : "cc");
   }

   return res;
}

inline
unsigned int __shrd(unsigned int into, unsigned int from, unsigned int c)
{
   unsigned int res;

   if (__builtin_constant_p(into) && 
       __builtin_constant_p(from) && 
       __builtin_constant_p(c))
   {
      res = (into >> c) | (from << (32 - c));
   }
   else
   {
      asm("shrd %b3, %2, %0"
          : "=rm" (res)
          : "0" (into), "r" (from), "ic" (c)
          : "cc");
   }

   return res;
}

my_uint128_t lshift_uint128 (const my_uint128_t a, unsigned int b)
{
   my_uint128_t res;

   if (b < 32) {
      res.x = a.x << b;
      res.y = __shld(a.y, a.x, b);
      res.z = __shld(a.z, a.y, b);
      res.w = __shld(a.w, a.z, b);
   } else if (b < 64) {
      res.x = 0;
      res.y = a.x << (b - 32);
      res.z = __shld(a.y, a.x, b - 32);
      res.w = __shld(a.z, a.y, b - 32);
   } else if (b < 96) {
      res.x = 0;
      res.y = 0;
      res.z = a.x << (b - 64);
      res.w = __shld(a.y, a.x, b - 64);
   } else if (b < 128) {
      res.x = 0;
      res.y = 0;
      res.z = 0;
      res.w = a.x << (b - 96);
   } else {
      memset(&res, 0, sizeof(res));
   }

   return res;
}

my_uint128_t rshift_uint128 (const my_uint128_t a, unsigned int b)
{
   my_uint128_t res;

   if (b < 32) {
      res.x = __shrd(a.x, a.y, b);
      res.y = __shrd(a.y, a.z, b);
      res.z = __shrd(a.z, a.w, b);
      res.w = a.w >> b;
   } else if (b < 64) {
      res.x = __shrd(a.y, a.z, b - 32);
      res.y = __shrd(a.z, a.w, b - 32);
      res.z = a.w >> (b - 32);
      res.w = 0;
   } else if (b < 96) {
      res.x = __shrd(a.z, a.w, b - 64);
      res.y = a.w >> (b - 64);
      res.z = 0;
      res.w = 0;
   } else if (b < 128) {
      res.x = a.w >> (b - 96);
      res.y = 0;
      res.z = 0;
      res.w = 0;
   } else {
      memset(&res, 0, sizeof(res));
   }

   return res;
}

3)添加可能受益于asm。你可以试试这个:

struct my_uint128_t
{
   unsigned int x;
   unsigned int y;
   unsigned int z;
   unsigned int w;
};

my_uint128_t add_uint128 (const my_uint128_t a, const my_uint128_t b)
{
   my_uint128_t res;

    asm ("addl %5, %[resx]\n\t"
         "adcl %7, %[resy]\n\t"
         "adcl %9, %[resz]\n\t"
         "adcl %11, %[resw]\n\t"
         : [resx] "=&r" (res.x), [resy] "=&r" (res.y), 
           [resz] "=&r" (res.z), [resw] "=&r" (res.w)
         : "%0"(a.x), "irm"(b.x), 
           "%1"(a.y), "irm"(b.y), 
           "%2"(a.z), "irm"(b.z), 
           "%3"(a.w), "irm"(b.w)
         : "cc");

   return res;
}

我刚刚将其删除,因此请自担风险。我没有爱迪生,但这适用于x86。

更新:如果您只是进行积累(请考虑to += from而不是上面的代码c = a + b),此代码可能会为您提供更好的服务:

inline
void addto_uint128 (my_uint128_t *to, const my_uint128_t from)
{
   asm ("addl %[fromx], %[tox]\n\t"
        "adcl %[fromy], %[toy]\n\t"
        "adcl %[fromz], %[toz]\n\t"
        "adcl %[fromw], %[tow]\n\t"
        : [tox] "+&r"(to->x), [toy] "+&r"(to->y), 
          [toz] "+&r"(to->z), [tow] "+&r"(to->w)
        : [fromx] "irm"(from.x), [fromy] "irm"(from.y), 
          [fromz] "irm"(from.z), [fromw] "irm"(from.w)
        : "cc");
}

答案 1 :(得分:2)

如果使用外部库是一个选项,请查看this question。您可以使用TTMath这是一个非常简单的标题,用于大精度数学。在32位体系结构ttmath:UInt<4>上将创建一个128位int类型,其中包含四个32位肢体。

如果您必须自己编写,那么SO上已经有很多解决方案,我会在这里总结一下


对于加法和减法,它非常简单直接,只需添加/减去单词(大型int库通常称为肢体)从最低有效到高有效当然还有随身携带。

typedef struct INT128 {
    uint64_t H, L;
} my_uint128_t;

inline my_uint128_t add(my_uint128_t a, my_uint128_t b)
{
    my_uint128_t c;
    c.L = a.L + b.L;
    c.H = a.H + b.H + (c.L < a.L);  // c = a + b
    return c;
}

可以使用Compiler Explorer

检查装配输出

编译器已经可以为双字操作生成有效的代码,但是许多人在使用高级语言编译多字操作时使用“with with carry”并不够智能,如问题所示{{3 }}。因此,使用上面的2 long long将使它不仅更具可读性,而且更容易让编译器发出更高效的代码。

如果仍然不符合您的性能要求,则必须使用内在函数或在汇编中写入。要将bignum中存储的128位值添加到{eax,ebx,ecx,edx}中的128位值,可以使用以下代码

add edx, [bignum]
adc ecx, [bignum+4]
adc ebx, [bignum+8]
adc eax, [bignum+12]

Clang

的等效内在值为efficient 128-bit addition using carry flag
unsigned *x, *y, *z, carryin=0, carryout;
z[0] = __builtin_addc(x[0], y[0], carryin, &carryout);
carryin = carryout;
z[1] = __builtin_addc(x[1], y[1], carryin, &carryout);
carryin = carryout;
z[2] = __builtin_addc(x[2], y[2], carryin, &carryout);
carryin = carryout;
z[3] = __builtin_addc(x[3], y[3], carryin, &carryout);

您需要将内在函数更改为编译器支持的内在函数,例如like this__builtin_uadd_overflow in gcc MSVCICC

有关更多信息,请阅读这些


对于位移,您可以在问题Bitwise shift operation on a 128-bit number中找到C解决方案。这是一个简单的左移,但您可以展开递归调用以获得更高的性能

_addcarry_u32

可以在问题128-bit shifts using assembly language?

中找到小于32位移位的程序集
void shiftl128 (
    unsigned int& a,
    unsigned int& b,
    unsigned int& c,
    unsigned int& d,
    size_t k)
{
    assert (k <= 128);
    if (k >= 32) // shifting a 32-bit integer by more than 31 bits is "undefined"
    {
        a=b;
        b=c;
        c=d;
        d=0;
        shiftl128(a,b,c,d,k-32);
    }
    else
    {
        a = (a << k) | (b >> (32-k));
        b = (b << k) | (c >> (32-k));
        c = (c << k) | (d >> (32-k));
        d = (d << k);
    }
}

可以类似地实施右移,或者只是从上面链接的问题中复制


乘法和除法要复杂得多,您可以在问题Efficient Multiply/Divide of two 128-bit Integers on x86 (no 64-bit)中引用解决方案:

shld    edx, ecx, cl
shld    ecx, ebx, cl
shld    ebx, eax, cl
shl     eax, cl

您还可以使用代码

找到许多相关问题