GMP/MPFR肢体应该如何解读?

How should GMP/MPFR limbs be interpreted?

任意精度库 GMP 和 MPFR 使用机器字大小整数的堆分配数组来存储构成高精度的分支 number/mantissa。

应该如何解释这个肢体数组以恢复任意精度整数?换句话说:对于 N 个肢体,每个肢体都持有 B 位,我应该如何解释它们以恢复 N*B 位数?

肢体大小真的会影响内存中的表示吗(见下文)?如果是这样,这背后的原理是什么?


背景:

我写了一个小程序来查看表示的内部,但我对所看到的内容感到困惑。肢体似乎以 最高有效数字 顺序排列,而肢体本身采用本机最低有效数字格式。当使用 32 位字表示 64 位字 0xAAAABBBBCCCCDDDD 并且精度固定为 128 位时,我看到:

% c++ limbs.cpp -lgmp -lmpfr -o limbs && ./limbs
ccccdddd|aaaabbbb|00000000|00000000
00000000|00000000|ccccdddd|aaaabbbb

这似乎意味着内存中的表示可以不能作为一串位读回以恢复任意精度数字(例如,如果将其加载到寄存器中在支持 N*B 大小单词的机器上)。此外,这似乎也表明肢体大小改变了表示,因此我无法在不知道使用哪个肢体大小进行序列化的情况下反序列化数字。

这是我的测试程序(使用带有 __GMP_SHORT_LIMB 宏的 32 位肢体):

#define __GMP_SHORT_LIMB
#include <gmp.h>
#include <mpfr.h>

#include <iomanip>
#include <iostream>

constexpr int PRECISION = 128;

void PrintLimbs(mp_limb_t const *const limbs) {
  std::cout << std::hex;
  constexpr int NUM_LIMBS = PRECISION / (8 * sizeof(mp_limb_t));
  for (int i = 0; i < NUM_LIMBS; ++i) {
    std::cout << std::setfill('0') << std::setw(2 * sizeof(mp_limb_t)) << limbs[i];
    if (i < NUM_LIMBS - 1) {
      std::cout << "|";
    }
  }
  std::cout << "\n";
}

int main() {
  {  // GMP
    mpz_t num;
    mpz_init2(num, PRECISION);
    mpz_set_ui(num, 0xAAAABBBBCCCCDDDD);
    PrintLimbs(num->_mp_d);
    mpz_clear(num);
  }
  {  // MPFR
    mpfr_t num;
    mpfr_init2(num, PRECISION);
    mpfr_set_ui(num, 0xAAAABBBBCCCCDDDD, MPFR_RNDN);
    PrintLimbs(num->_mpfr_d);
    mpfr_clear(num);
  }
  return 0;
}

3 个对字节表示很重要的事情:

  • 肢体尺寸取决于您的机器和选择的 ABI。实际大小也受到 钉子 的可选存在的影响(实验性特征,因此四肢不太可能有钉子)。 MPFR 不支持钉子的存在。
  • 内存中的肢体表示遵循机器的字节顺序。
  • 肢体首先存储最不重要的肢体(a.k.a。小端)。

注意最后两点,在同一个大端机器上,数组的字节表示将取决于肢体大小。

关于四肢阵列的大小,这取决于类型。比如GMP的mpn层,完全由用户来处理。

对于MPFR,大小是从mpfr_t对象的精度推导出来的;如果精度不是 limb bitsize 的倍数,则尾随位始终设置为 0。还要注意,分配的内存可能比实际使用的内存多,并且不能与数组的大小混淆;你可以忽略这个事实,因为未使用的数据总是在实际的肢体数组之后。

EDIT 关于基本原理:操纵肢体而不是字节是出于速度原因。那么我想,出于两个原因,已经选择小字节序来表示肢体数组。首先,它使基本运算(加法、减法、乘法)更容易实现并且可能更快。其次,这更适合实现算术模 2^K,特别是当 K 可能改变时。

它终于让我满意了。肢体大小不会影响内存中的表示。

GMP/MPFR中的数据始终以小端格式存储,即使被解释为字节串横跨四肢。但是 x86 上的寄存器是大端的。

打印四肢时的不一致结果来自于从记忆中回读时如何解释单词。当加载到寄存器中时,内存从小端(如何存储在内存中)重新解释为大端(如何存储在寄存器中)。

我修改了下面的示例,以显示实际上 我们重新解释内存的字数 是如何影响内容的打印方式的,因为输出将是无论使用 32 位还是 64 位肢体都一样:

#define __GMP_SHORT_LIMB
#include <gmp.h>
#include <mpfr.h>

#include <iomanip>
#include <iostream>
#include <cstdint>

constexpr int PRECISION = 128;

template <typename InterpretAs>
void PrintLimbs(mp_limb_t const *const limbs) {
  constexpr int LIMB_BITS = 8 * sizeof(InterpretAs); 
  constexpr int NUM_LIMBS = PRECISION / LIMB_BITS;
  std::cout << LIMB_BITS << "-bit: ";
  for (int i = 0; i < NUM_LIMBS; ++i) {
    const auto limb = reinterpret_cast<InterpretAs const *>(limbs)[i];
    for (int b = 0; b < LIMB_BITS; ++b) {
      if (b > 0 && b % 16 == 0) {
        std::cout << " ";
      }
      uint64_t bit = (limb >> (LIMB_BITS - 1 - b)) & 0x1; 
      std::cout << bit; 
    }
    if (i < NUM_LIMBS - 1) {
      std::cout << "|";
    }
  }
  std::cout << "\n";
}

int main() {
  uint64_t literal = 0b1111000000000000000000000000000000000000000000000000000000001001;
  {  // GMP
    mpz_t num;
    mpz_init2(num, PRECISION);
    mpz_set_ui(num, literal);
    std::cout << "GMP where limbs are interpreted as:\n";
    PrintLimbs<uint64_t>(num->_mp_d);
    PrintLimbs<uint32_t>(num->_mp_d);
    PrintLimbs<uint16_t>(num->_mp_d);
    mpz_clear(num);
  }
  {  // MPFR
    mpfr_t num;
    mpfr_init2(num, PRECISION);
    mpfr_set_ui(num, literal, MPFR_RNDN);
    std::cout << "MPFR where limbs are interpreted as:\n";
    PrintLimbs<uint64_t>(num->_mpfr_d);
    PrintLimbs<uint32_t>(num->_mpfr_d);
    PrintLimbs<uint16_t>(num->_mpfr_d);
    mpfr_clear(num);
  }
  return 0;
}

这会打印(无论肢体大小如何):

GMP where limbs are interpreted as:
64-bit: 1111000000000000 0000000000000000 0000000000000000 0000000000001001|0000000000000000 0000000000000000 0000000000000000 0000000000000000
32-bit: 0000000000000000 0000000000001001|1111000000000000 0000000000000000|0000000000000000 0000000000000000|0000000000000000 0000000000000000
16-bit: 0000000000001001|0000000000000000|0000000000000000|1111000000000000|0000000000000000|0000000000000000|0000000000000000|0000000000000000
MPFR where limbs are interpreted as:
64-bit: 0000000000000000 0000000000000000 0000000000000000 0000000000000000|1111000000000000 0000000000000000 0000000000000000 0000000000001001
32-bit: 0000000000000000 0000000000000000|0000000000000000 0000000000000000|0000000000000000 0000000000001001|1111000000000000 0000000000000000
16-bit: 0000000000000000|0000000000000000|0000000000000000|0000000000000000|0000000000001001|0000000000000000|0000000000000000|1111000000000000