Самый быстрый способ преобразовать двоичное в десятичное?

У меня есть четыре беззнаковых 32-битных целых числа, представляющих беззнаковое 128-битное целое число в обратном порядке:

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

Я хотел бы преобразовать это число в его десятичное строковое представление и вывести его в файл.

Прямо сейчас я использую функцию bigint_divmod10, чтобы разделить число на 10, отслеживая остаток. Я вызываю эту функцию несколько раз, выводя остаток в виде цифры, пока число не станет нулевым. Это довольно медленно. Это самый быстрый способ сделать это? Если да, то есть ли хитрый способ реализовать эту функцию, которого я не вижу? Я пробовал смотреть get_str.c GMP, но мне это непонятно.

РЕДАКТИРОВАТЬ: вот самый быстрый код, который я смог придумать для функции 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;
}

где функция добавления определяется как:

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;
            }
        }
    }
}

person ianh    schedule 06.11.2009    source источник
comment
почему не шестнадцатеричное, а десятичное строковое представление? в шестнадцатеричный быстрее.   -  person Test    schedule 06.11.2009
comment
Вы на 100% уверены, что это критично для производительности вашей программы, и, таким образом, гарантирует, что вы потратите время на ее настройку, а ваши последователи попытаются понять, в чем весь этот беспорядок?   -  person vonbrand    schedule 05.02.2013


Ответы (6)


Это зависит от того, что еще вы делаете с числами. Вы можете найти компромисс между небольшой потерей эффективности использования пространства и умеренной потерей эффективности арифметики с высокой точностью в обмен на очень эффективное преобразование в десятичную форму и обратно. Ключ состоит в том, чтобы выполнять арифметические операции с высокой точностью с основанием, которое является степенью 10, а не степенью 2.

Например, вы можете использовать основание 10 000, когда вы упаковываете одну цифру в 16-битное слово и выполняете арифметические операции с цифрами в 32-битных целых числах. (Если вы работаете на 64-битной машине, вы можете удвоить это количество и сделать базу 1 000 000 000.) Этот вид кода относительно эффективен во времени, хотя и не так быстр, как использование собственной мощности двойки, потому что вы не можете воспользоваться преимуществами бит переноса на оборудовании. И вы не можете представить столько же целых чисел в одном и том же количестве битов. Но это умение преобразовывать в десятичные числа и обратно, потому что вы можете преобразовывать отдельные цифры без какого-либо деления в столбик.

Если вам нужно представить полный диапазон чисел от нуля до ((1 << 128) - 1), вы все равно можете это сделать, но добавьте дополнительную цифру, чтобы ваши числа были больше.

Если окажется, что вам действительно нужно дополнительное пространство / скорость (возможно, вы делаете много криптографических 128-битных вычислений), тогда метод одновременного div / mod на 10 - самый быстрый из известных мне методов. Единственная уловка заключается в том, что если маленькие целые числа являются обычным явлением, вы можете обрабатывать их особым образом. (То есть, если все три наиболее значимых 32-битных слова равны нулю, просто используйте собственное деление для преобразования.)

Есть ли хитрый способ реализовать эту функцию, которого я не вижу?

В интерфейсах и реализациях C Дэйва Хэнсона есть длинная глава о многоточной арифметике. Разделение большого числа на одну цифру - особый случай, в котором есть такая эффективная реализация:

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;
}

Для полного понимания полезно иметь книгу, но исходный код по-прежнему намного проще для понимания, чем исходный код GNU. И вы можете легко адаптировать его для использования базы 10,000 (в настоящее время она использует базу 256).

Резюме: если ваше узкое место в производительности - это преобразование в десятичные числа, реализуйте арифметику с множественной точностью с основанием, равным 10. Если собственный размер слова вашей машины равен 32, и вы используете код C, используйте 10 000 в 16-битном слове.

person Norman Ramsey    schedule 07.11.2009

Если ваши значения в основном меньше ULLONG_MAX (18446744073709551615), я бы попытался использовать для них sprintf(buf,"%llu",ullong_val). Бьюсь об заклад, это довольно хорошо оптимизировано в стандартной библиотеке, но синтаксический анализ формата займет несколько циклов.

В противном случае я бы создал функцию bigint_divmod1000000000 (или лучше назовите mod10to9) и использовал бы ее. Потребовалось бы в 9 раз меньше делений, чем bigint_divmod10.

person Tometzky    schedule 06.11.2009
comment
Такие огромные функции divmod на самом деле работают медленнее (я пробовал). - person ianh; 07.11.2009

Таблица поиска 8 бит. У вас может быть 4 таблицы поиска по 256 чисел. Первая - от 0 до 256 для байтов LSB, вторая таблица - это первая таблица, умноженная на 256 и так далее.

ТАК, когда вам нужен ваш номер, суммируйте числа из таблицы поиска. Когда вы добавляете, вы можете добавить его как bunary и пройти один проход по каждому байту, чтобы исправить потоки переполнения.

Номер примера 0x12345678 В первой таблице поиска находится под адресом (0x78 = 120), поэтому 0x010200 - это первое число во второй таблице под (0x56 = 87), это 0x0202000106 (0x56 в dec - 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;
}
person Luka Rahne    schedule 06.11.2009
comment
Числа в вопросе 128-битные; поправьте меня, если я ошибаюсь, но ваш ответ, похоже, предполагает 32-битные числа. - person ianh; 07.11.2009
comment
Насколько мне известно, я сделал правильные предположения. На первом шаге вы сделали 4 добавления 128-битных чисел, используя lookuptable (всего 16 добавлений), на самом деле немного меньше, потому что вы знаете, что байт LSB не превышает 32 бита. Итак, для байта LSB только одно добавление вместо 4. Я обнаружил ошибку и внес изменения в объяснение. вместо 0x120 есть 0x010200 - person Luka Rahne; 09.11.2009
comment
val[i] & 10 (побитовое И с 0b1010) не имеет смысла. Это не остаток. Сдвиг / маска работает только для основ со степенью двойки, например, шестнадцатеричной или восьмеричной. Наименее значимая десятичная цифра зависит от всех битов, поэтому справочная таблица вообще не работает. - person Peter Cordes; 05.01.2018
comment
@PeterCordes Это правильно. Суть в том, что нужны только дополнения и поиски в небольших таблицах. - person Luka Rahne; 05.01.2018
comment
Я не совсем уверен, как работает ваш алгоритм. Ваш ответ не очень хорошо это объясняет. Я не понимаю, почему вы используете шестнадцатеричные числа, когда OP хочет десятичную строку. Вы строите двоичное число, шестнадцатеричные цифры которого имеют значения десятичных цифр исходного числа? Я также не понимаю, какие типы у ваших переменных цикла. например s BigInteger? (достаточно большого, чтобы вместить число с таким количеством шестнадцатеричных цифр, сколько десятичных цифр может быть на выходе?). Какой тип val[]? - person Peter Cordes; 05.01.2018
comment
32-битные числа @PeterCordes используются для векторизации. Длинное двоичное число сначала разбивается на 8-битные блоки (байты). Тогда каждый байт зависит от индекса и значения, представленного в виде десятичного числа. Об этом можно прочитать в таблице. Мы складываем эти числа вместе и окончательно исправляем переполнение. Окончательное решение использует только таблицы сложения и поиска, но не использует ветвление (операторы if). Я не уверен, как это соотносится с другими оптимизированными решениями. - person Luka Rahne; 05.01.2018

Для справки в будущем, вместо реализации типа uint128, я просто использовал символы строки напрямую. Это оказалось намного быстрее, чем переход от строки к uint128 и обратно.

person ianh    schedule 07.11.2009
comment
Это имеет смысл, если вы выполняете всего пару операций перед преобразованием в строку. Использование 32-битных фрагментов с основанием 10 ^ 9 может хорошо работать и быть хорошим компромиссом между пространством и производительностью сложения / вычитания и деления на (степени) 10. Я использовал это , чтобы вычислить первые 1000 цифр Фибоначчи (10 ^ 9) за разумное время (~ 80 секунд), сохраняя только первые 1009 цифр после каждого шага, используя деление на 10. Всего 105 байт кода x86 с использованием сравнения для генерации переноса и переноса на 10 ^ 9. :) - person Peter Cordes; 05.01.2018

Наиболее быстрое ускорение будет происходить за счет встраивания преобразования, а не вызова функций; это может быть просто отметка bigint_divmod10() inline или использование профильной оптимизации, предлагаемой вашим компилятором.

person Will    schedule 06.11.2009

Я знаю, что это старый вопрос, но я хочу внести свой вклад, поскольку никто не нашел способа избежать цикла разделения. В нем используется 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

person superbem    schedule 03.07.2013
comment
Вы конвертируете массив разовых значений в двоичное целое число. (Причем очень неэффективно). OP хочет эффективно преобразовать uint128_t в строку. - person Peter Cordes; 05.01.2018