【问题标题】:Fastest way to convert binary to decimal?将二进制转换为十进制的最快方法?
【发布时间】:2010-12-13 17:57:08
【问题描述】:

我有四个无符号 32 位整数,代表一个无符号 128 位整数,按小端序排列:

typedef struct {
    unsigned int part[4];
} bigint_t;

我想将此数字转换为十进制字符串表示形式并将其输出到文件中。

现在,我正在使用bigint_divmod10 函数将数字除以 10,并记录余数。我反复调用这个函数,将余数作为数字输出,直到数字为零。这很慢。这是最快的方法吗?如果是这样,是否有一种聪明的方法来实现我没有看到的这个功能?我试过查看 GMP 的 get_str.c,但我发现它非常难以理解。

编辑:这是我能够为 divmod10 功能提出的最快代码:

static unsigned uint128_divmod10(uint128 *value)
{
    unsigned int a = value->word[3];
    unsigned int b = value->word[2];
    unsigned int c = value->word[1];
    unsigned int d = value->word[0];

    unsigned int diva = a / 5;
    unsigned int divb = b / 5;
    unsigned int divc = c / 5;
    unsigned int divd = d / 5;

    value->word[3] = diva;
    value->word[2] = divb;
    value->word[1] = divc;
    value->word[0] = divd;

    unsigned int moda = a - diva*5;
    unsigned int modb = b - divb*5;
    unsigned int modc = c - divc*5;
    unsigned int modd = d - divd*5;

    unsigned int mod = 0;
    mod += moda;
    unsigned int carryb = mod*858993459;
    mod += modb;
    if (mod >= 5) {
        mod -= 5;
        carryb++;
    }
    unsigned int carryc = mod*858993459;
    mod += modc;
    if (mod >= 5) {
        mod -= 5;
        carryc++;
    }
    unsigned int carryd = mod*858993459;
    mod += modd;
    if (mod >= 5) {
        mod -= 5;
        carryd++;
    }

    uint128_add(value, carryd, 0);
    uint128_add(value, carryc, 1);
    uint128_add(value, carryb, 2);

    if (value->word[0] & 1) {
        mod += 5;
    }
    uint128_shift(value, -1);
    return mod;
}

其中add函数定义为:

static void uint128_add(uint128 *value, unsigned int k, unsigned int pos)
{
    unsigned int a = value->word[pos];
    value->word[pos] += k;
    if (value->word[pos] < a) {
        // overflow
        for (int i=pos+1; i<4; i++) {
            value->word[i]++;
            if (value->word[i]) {
                break;
            }
        }
    }
}

【问题讨论】:

  • 为什么不是十六进制而是十进制字符串表示?到十六进制更快。
  • 您是否 100% 确定这对您的程序的性能至关重要,因此值得您花时间对其进行调整,并且您的继任者会试图了解这些混乱的原因?

标签: c bigint bignum 128-bit


【解决方案1】:

最直接的加速将来自内联转换而不是调用函数;它可以像标记 bigint_divmod10() inline 一样简单,或者使用编译器提供的配置文件引导优化。

【讨论】:

    【解决方案2】:

    如果您的值大多小于 ULLONG_MAX (18446744073709551615),我会尝试使用 sprintf(buf,"%llu",ullong_val)。我敢打赌,这在标准库中已经得到了很好的优化,但是格式的解析需要一些周期。

    否则我会创建一个bigint_divmod1000000000(或更好的名称 mod10to9)函数并使用它。它需要的除数比bigint_divmod10 少 9 倍。

    【讨论】:

    • 像这样的巨大 divmod 函数实际上更慢(我试过)。
    【解决方案3】:

    8 位查找表。 您可以有 4 个 256 个数字的查找表。 第一个是从 0-256 的 LSB 字节,第二个表是第一个表乘以 256,依此类推。

    所以,当您需要您的号码时,请从查找表中汇总号码。 添加时,您可以添加为 bunary,然后再通过每个字节来修复 owerflows。

    示例 号码 0x12345678 在第一个查找表中有地址(0x78 = 120) 所以 0x010200 是第一个数字 在(0x56=87)下的第二个表中是 0x0202000106(12 月中的 0x56 是 22016) 在第三张表中你会有 0x03040007080702 在 0x12 的最后一个标签下,你有 0x030001090809080808(这不适合 32 位算术,但你已经知道了)

    然后将这些数字相加(作为二进制数字)并逐字节进行溢出 for循环中的代码类似于

    s=carry+val[i];
    val[i]=val[i]&10
    carry=s/10; 
    //you can put last two operations in table
    

    如果我们计算为此所需的操作。

    1.(查看表并添加)4 个查找表。 16个添加(请记住,当您不需要进行溢出时,因为它们不会发生)
    2. 每步一关 3 操作 16 步通过。

    被动上限 6*16 = 100 次操作。

    编辑:

    这里是 c++ 代码,比简单实现快 30%。

    #include <iostream>
    #include <stdint.h>
    #include <array>
    
    static uint64_t lu[4][256];
    
    constexpr uint64_t lookup_value(uint64_t n) {
      uint64_t r = 0;
      uint64_t t = 1;
      while (n) {
        uint64_t rem = n % 10;
        n /= 10;
        r += rem * t;
        t *= 256;
      }
      return r;
    }
    
    void make_lu() {
      uint64_t step = 1;
      for (int j = 0; j < 4; ++j) {
        uint64_t n = 0;
        for (int i = 0; i < 256; ++i) {
          lu[j][i] = lookup_value(n);
          n += step;
        }
        step *= 256;
      }
    }
    
    struct DivMod {
      uint8_t div;
      uint8_t rem;
    };
    
    static DivMod dm[256];
    
    void make_dm() {
      for (int i = 0; i < 256; ++i) {
        dm[i].div = i / 10;
        dm[i].rem = i % 10;
      }
    }
    
    void init() {
      make_lu();
      make_dm();
    }
    
    uint64_t b2d(uint64_t n) {
      uint64_t r = 0;
      for (int i = 0; i < 4; ++i) {
        r += lu[i][(n >> (i * 8)) & 0xff];
      }
      uint64_t r2 = 0;
      uint64_t of = 0;
      for (int i = 0; i < 8; ++i) {
        uint64_t v = ((r >> (i * 8)) & 0xff) + of;
        DivMod &x = dm[v];
        of = x.div;
        r2 += uint64_t(x.rem) << (i * 8);
      }
      return r2;
    }
    
    int main() {
      init();
      uint64_t n;
      std::cin >> n;
      std::cout << std::hex << b2d(n) << "\n";
      return 0;
    }
    

    【讨论】:

    • 问题中的数字是128位的;如果我错了,请纠正我,但您的答案似乎假设为 32 位数字。
    • 据我所知,我做出了正确的假设。在第一步中,您使用查找表(总共 16 个添加)添加了 4 次 128 位数字,实际上少了一点,因为您知道 LSB 字节不超过 32 位。所以对于 LSB 字节只有一个添加而不是 4。我发现了错误并在解释中做了更改。插入 0x120 有 0x010200
    • val[i] &amp; 10(按位与0b1010)没有意义。这不是余数。 Shift/mask 仅适用于 2 的幂的碱基,例如十六进制或八进制。最不重要的十进制数字取决于所有位,因此查找表根本不起作用。
    • @PeterCordes 没错。要点是只需要在小表中进行添加和查找。
    • 我不确定您的算法究竟是如何工作的。您的回答没有很好地解释它。我不明白为什么当 OP 需要十进制字符串时你使用十六进制数字。您是否正在构建一个二进制数,其十六进制数字具有原始数字的十进制数字的值?我也不明白你的循环变量有什么类型。例如s 是大整数吗? (大到足以容纳一个具有与输出一样多的十六进制数字的数字可以有十进制数字?)。 val[]是什么类型的?
    【解决方案4】:

    为了以后参考,我没有实现 uint128 类型,而是直接使用了字符串的字符。结果证明这比从字符串到 uint128 再返回要快得多。

    【讨论】:

    【解决方案5】:

    这取决于您对这些数字还做了什么。您可以权衡空间效率的轻微损失和多精度算术效率的适度损失,以换取非常有效的十进制转换。关键是使用 10 的幂而不是 2 的幂进行多精度算术。

    例如,您可以使用以 10,000 为基数,将一位数字打包成一个 16 位字,然后对 32 位整数中的数字进行算术运算。 (如果您使用的是 64 位机器,则可以将其加倍并以 1,000,000,000 为底。)这种代码在时间上相对有效,尽管不如使用 2 的本机幂快,因为您无法利用硬件上的进位位。 而且你不能用相同的位数表示尽可能多的整数。 但它是转换十进制和从十进制转换的高手,因为您无需任何长除法即可转换单个数字。

    如果您需要表示从零到((1 &lt;&lt; 128) - 1) 的整个数字范围,您仍然可以这样做,但添加一个额外的数字,这样您的数字会更大。

    如果事实证明您确实需要额外的空间/速度(也许您正在执行大量加密 128 位计算),那么同时 div/mod by 10 的方法是我所知道的最快的方法。唯一的另一个技巧是,如果小整数很常见,您可以专门处理它们。 (即如果三个最重要的 32 位字都为零,则只需使用本机除法转换即可。)

    有没有一种聪明的方法来实现这个我没有看到的功能?

    Dave Hanson 的 C Interfaces and Implementations 有一个关于多精度算术的冗长章节。将大数除以一位数是一种具有这种高效实现的特殊情况:

    int XP_quotient(int n, T z, T x, int y) {
        int i;
        unsigned carry = 0;
        for (i = n - 1; i >= 0; i--) {
            carry = carry*BASE + x[i];
            z[i] = carry/y;
            carry %= y;
        }
        return carry;
    }
    

    为了充分理解,拥有这本书确实很有帮助,但是source code 仍然比 GNU 源代码更容易理解。您可以轻松地调整它以使用基数 10,000(它目前使用基数 256)。

    总结:如果您的性能瓶颈是转换为十进制,请实现 以 10 的幂为底的多精度算术。如果您机器的原生字长为 32,并且您使用的是 C 代码,请在 16 位字中使用 10,000。

    【讨论】:

      【解决方案6】:

      我知道这个问题很老,但我想做出贡献,因为没有人能避免分裂周期。这个使用 pow2,我没有测试过基准,但理论上应该比任何其他的都快,并且也可以在 pow 函数中进行调整。

      #include <iostream>
      #include <cmath>
      using namespace std;
      
      #define MathBintodec(arr,len)({int dec=0;int ci_;for(ci_=len;ci_--;)dec+=arr[ci_]*pow(2,len-ci_-1);dec;})
      
      int main(){
          int r[]={1,0,0,1,0,0};
          cout<<MathBintodec(r,6)<<endl;
      }
      

      输出: 36

      【讨论】:

      • 您正在将位值数组转换为二进制整数。 (而且效率很低)。 OP 希望有效地将uint128_t 转换为字符串。
      猜你喜欢
      • 2012-06-12
      • 1970-01-01
      • 1970-01-01
      • 2012-06-26
      • 1970-01-01
      • 1970-01-01
      • 2019-01-27
      • 2012-12-22
      • 2016-06-27
      相关资源
      最近更新 更多