【问题标题】:How can I add and subtract 128 bit integers in C or C++ if my compiler does not support them?如果我的编译器不支持,如何在 C 或 C++ 中加减 128 位整数?
【发布时间】:2009-04-12 05:19:32
【问题描述】:

我正在为一长串 128 位数字编写压缩器。我想将数字存储为差异 - 仅存储数字之间的差异而不是数字本身,因为我可以将差异打包成更少的字节,因为它们更小。

但是,对于压缩,我需要减去这些 128 位值,而对于解压缩,我需要添加这些值。我的编译器的最大整数大小为 64 位宽。

任何人有任何想法来有效地做到这一点?

【问题讨论】:

    标签: c++ integer 128-bit


    【解决方案1】:

    如果您只需要加法和减法,并且您已经拥有二进制形式的 128 位值,那么库可能会很方便,但并非绝对必要。自己做这个数学很简单。

    我不知道您的编译器对 64 位类型使用什么,所以我将使用 INT64 和 UINT64 来处理有符号和无符号 64 位整数。

    class Int128
    {
    public:
        ...
        Int128 operator+(const Int128 & rhs)
        {
            Int128 sum;
            sum.high = high + rhs.high;
            sum.low = low + rhs.low;
            // check for overflow of low 64 bits, add carry to high
            if (sum.low < low)
                ++sum.high;
            return sum;
        }
        Int128 operator-(const Int128 & rhs)
        {
            Int128 difference;
            difference.high = high - rhs.high;
            difference.low = low - rhs.low;
            // check for underflow of low 64 bits, subtract carry to high
            if (difference.low > low)
                --difference.high;
            return difference;
        }
    
    private:
        INT64  high;
        UINT64 low;
    };
    

    【讨论】:

    • 你可以只使用 stdint.h。
    【解决方案2】:

    看看GMP

    #include <stdio.h>
    #include <gmp.h>
    
    int main (int argc, char** argv) {
        mpz_t x, y, z;
        char *xs, *ys, *zs;
        int i;
        int base[4] = {2, 8, 10, 16};
    
        /* setting the value of x in base 10 */
        mpz_init_set_str(x, "100000000000000000000000000000000", 10);
    
        /* setting the value of y in base 16 */
        mpz_init_set_str(y, "FF", 16);
    
        /* just initalizing the result variable */
        mpz_init(z);
    
        mpz_sub(z, x, y);
    
        for (i = 0; i < 4; i++) {
            xs = mpz_get_str(NULL, base[i], x);
            ys = mpz_get_str(NULL, base[i], y);
            zs = mpz_get_str(NULL, base[i], z);
    
            /* print all three in base 10 */
            printf("x = %s\ny = %s\nz = %s\n\n", xs, ys, zs);
    
            free(xs);
            free(ys);
            free(zs);
        }
    
        return 0;
    }
    

    输出是

    x = 10011101110001011010110110101000001010110111000010110101100111011111000000100000000000000000000000000000000
    y = 11111111
    z = 10011101110001011010110110101000001010110111000010110101100111011111000000011111111111111111111111100000001
    
    x = 235613266501267026547370040000000000
    y = 377
    z = 235613266501267026547370037777777401
    
    x = 100000000000000000000000000000000
    y = 255
    z = 99999999999999999999999999999745
    
    x = 4ee2d6d415b85acef8100000000
    y = ff
    z = 4ee2d6d415b85acef80ffffff01
    

    【讨论】:

    • 如果我需要的不仅仅是加法和减法,我肯定会记住这个库。谢谢!
    • GMP 带有一个 C++ 接口 (#include &lt;gmpxx.h&gt;),使用起来更简单...
    【解决方案3】:

    完全是偶然发现这篇相对较旧的帖子,我认为有必要详细说明 Volte 先前的猜想,以使没有经验的读者受益。

    首先,128 位数字的有符号范围是 -2127 到 2127-1 而不是 -2127 到2127 原规定。

    其次,由于有限算术的循环特性,两个 128 位数字之间所需的最大差异是 -2127 到 2127-1,它具有128 位的存储先决条件,而不是 129。虽然 (2127-1) - (-2127) = 2128-1 其中明显大于我们最大的 2127-1 正整数,算术溢出总是确保任意两个 n 位数字之间的最近距离总是在 0 到 2 的范围内n-1 因此隐含 -2n-1 到 2n i>-1-1.

    为了澄清,让我们首先检查一个假设的 3 位处理器如何实现二进制加法。例如,考虑下表,它描述了 3 位整数的绝对无符号范围。

       0 = 000b
    1 = 001b
    2 = 010b
    3 = 011b
    4 = 100b
    5 = 101b
    6 = 110b
    7 = 111b ---> [溢出时循环回到 000b]

    从上表不难看出:

       001b(1) + 010b(2) = 011b(3)

    很明显,将这些数字中的任何一个与其数字补码相加总是会产生 2n-1:

       010b(2) + 101b([2 的补码] = 5) = 111b(7) = (23-1)

    由于两个 n 位数字相加导致 (n+1) 位结果时发生循环溢出,因此可以得出这些数字中的任何一个及其数字补码 + 1 将始终产生 0:

       010b(2) + 110b([2 的补码] + 1) = 000b(0)

    因此我们可以说[n的补码] + 1 = -n,所以n + [的补码n] + 1 = n + (-n) = 0。此外,如果我们现在知道 n + [complement n] + 1 = 0,则 n + [n - x] + 1 必须 = n - (n-x) = x.

    将其应用于我们原来的 3 位表会产生:

       0 = 000b = [0 的补码] + 1 = 0
    1 = 001b = [7 的补码] + 1 = -7
    2 = 010b = [6 的补码] + 1 = -6
    3 = 011b = [5 的补码] + 1 = -5
    4 = 100b = [4 的补码] + 1 = -4
    5 = 101b = [3 的补码] + 1 = -3
    6 = 110b = [2 的补码] + 1 = -2
    7 = 111b = [1 的补码] + 1 = -1 ---> [溢出时循环回到 000b]

    无论表示抽象是正数、负数还是两者的组合,正如带符号的补码算术所暗示的那样,我们现在有 2nn i>-bit 模式可以无缝地为正 0 到 2n-1 和负 0 到 -(2n)-1 范围在需要时。事实上,所有现代处理器都采用这样的系统来实现加法和减法运算的通用 ALU 电路。当 CPU 遇到 i1 - i2 减法指令时,它会在内部对 i2 执行 [补码 + 1] 操作,然后通过加法电路处理操作数以计算 i1 + [i2 的补码] + 1. 除了一个额外的进位/符号 XOR 门控溢出标志外,有符号和无符号加法以及隐含减法都是隐式的。

    如果我们将上表应用于输入序列 [-2n-1, 2n-1 sup>-1, -2n-1] 如 Volte 的原始回复中所述,我们现在能够计算以下 n 位微分:

    差异 #1:
    (2n-1-1) - (-2n-1) =
    3 - (-4) = 3 + 4 =
    (-1) = 7 = 111b

    差异 #2:
    (-2n-1) - (2n-1-1) =
    (-4) - 3 = (-4) + (5) =
    (-7) = 1 = 001b

    从我们的种子 -2n-1 开始,我们现在能够通过依次应用上述每个微分来重现原始输入序列:

       (-2n-1) + (diff #1) =
    (-4) + 7 = 3 =
    2n-1-1

       (2n-1-1) + (diff #2) =
    3 + (-7) = (-4) =
    -2n-1

    您当然可能希望对这个问题采取更哲学的方法,并推测为什么 2n 循环序列数需要超过 2n 循环顺序微分?

    塔利亚顿。

    【讨论】:

    • 我不得不说我理解了大约 5% 的帖子。我可以确认,使用我描述的算法,我可以将白名单中的 CLSID 压缩大约 20%。
    【解决方案4】:

    Boost 1.53 现在包括多精度:

    #include <boost/multiprecision/cpp_int.hpp>
    #include <iostream>
    
    // Requires Boost 1.53 or higher
    // build: g++ text.cpp
    
    int main()
    {
        namespace mp = boost::multiprecision;
    
        mp::uint128_t a = 4294967296;
        mp::uint256_t b(0);
        mp::uint512_t c(0);
    
        b = a * a;
        c = b * b;
    
        std::cout << "c: " << c << "\n";
        return 0;
    }
    

    输出:

    ./a.out
    c: 340282366920938463463374607431768211456
    

    【讨论】:

    • 轻松获得最佳答案。 Boost 可以直接替换本机类型,例如 double。 Boost 文档非常适合阅读,并提供了许多不错的示例代码。
    【解决方案5】:

    有很多关于大整数数学的文献。您可以使用免费提供的库之一(请参阅链接),也可以使用自己的库。虽然我应该警告你,但对于比加减法(和移位)更复杂的事情,你需要使用非平凡的算法。

    要进行加减运算,您可以创建一个包含两个 64 位整数的类/结构。您可以使用简单的学校数学来进行加法和减法。基本上,做你用铅笔和纸做的事情来增加或减少,仔细考虑进位/借位。

    搜索大整数。顺便说一句,最近版本的 VC++、IntelC++ 和 GCC 编译器具有 128 位整数类型,尽管我不确定它们是否像您希望的那样容易访问(它们旨在与 sse2/xmms 寄存器一起使用)。

    【讨论】:

    • 如果我尝试使用编译器的 __int128 类型,我会收到以下消息:c:\...\clsidcompressor.h(5) : error C4235: nonstandard extension used : '__int128' keyword not supported on这种架构
    • 您可能已将目标更改为没有上述 sse2/xmms 寄存器的目标,因此使用 __int128 没有意义。尝试搜索文档以查看是否可以使用它们。否则,确实有很多小型库可以完成这项工作。
    • Visual Studio 不会在 x86 或 x64 上使用 __int128。
    • 128 位整数与 SSE 完全无关,它们只使用 x86 上的基本 ALU。
    【解决方案6】:

    TomsFastMath 有点像 GMP(上面提到过),但是是公共领域,并且从一开始就设计得非常快(它甚至包含针对 x86、x86-64、ARM、SSE2、 PPC32 和 AVR32)。

    【讨论】:

    • GMP 在 LGPL 下,所以你可以做任何理性的人想做的事情:gmplib.org/manual/Copying.html#Copying
    • LGPL 仍有一些要求可能会让人三思而后行:您需要为用户提供能够将您的应用程序与更新/自定义/不同版本的 LGPL 库重新链接的能力.
    • @Michael:将 L 视为“共享库”而不是其他任何东西。如果您的应用是动态链接的,那么您已经提供了重新链接的必要功能,因为可以在 lib 路径中放置一个新的共享库。
    • 如果它有一个简单的目标并且做得很好,为什么还需要更新?
    【解决方案7】:

    另外值得注意的是:如果目标仅仅是通过预处理来改进数字流的压缩,那么预处理后的流不必由精确的算术差异组成。您可以使用 XOR (^) 代替 +-。优点是128位异或与64位部分的两个独立异或完全一样,既简单又高效。

    【讨论】:

      猜你喜欢
      • 2013-04-28
      • 2015-11-04
      • 1970-01-01
      • 2013-08-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多