Самый быстрый способ преобразовать BigInteger в десятичную (Base 10) строку?

Ответы до сих пор

Итак, вот расшифровка кода.

//Time: ~7s (linear loop algorithm)
//100,000! (456,574 decimal digits)
BigInteger bigIntVar = computeFactorial(100000);

//The first three here are just for comparison and are not actually Base 10.
bigIntVar.ToBase64String() //Time: 00.001s | Base 64 | Tetrasexagesimal
bigIntVar.ToString("x")    //Time: 00.016s | Base 16 | Hexadecimal
bigIntVar.ToBinaryString() //Time: 00.026s | Base 02 | Binary
bigIntVar.ToQuickString()  //Time: 11.200s | Base 10 | String Version
bigIntVar.ToQuickString()  //Time: 12.500s | Base 10 | StringBuilder Version
bigIntVar.ToString()       //Time: 13.300s | Base 10 | Original

Оригинальный вопрос

Я потратил на это много времени, поэтому мне нужна ваша помощь.

Это для личного проекта по вычислению огромных факториалов (например, 100 000!)

Вот мой код:

using (var stream = new StreamWriter(fileName + ".txt", false))
{
    stream.WriteLine(header);

    var timer = new Stopwatch();    
    timer.Restart();
    //This is the huge BigInteger holding the answer to 100,000!
    stream.WriteLine(saveFactorial.Output.ToString());         
    //Let me be clear: ToString() is directly causing the the 13sec time delay.
    //Not the stream.
    timer.Stop();                   
}

time = (timer.ElapsedMilliseconds / 1000.0).ToString() + "s"; 

MessageBox.Show(time);

На 100 000! на моей машине требуется около 7 секунд для вычисления (алгоритм линейного цикла).

Тем не менее, с этим стандартным кодом ввода-вывода для сохранения требуется 13 секунд.

Другими словами, для сохранения работы требуется больше времени, чем для ее скромного вычисления.

Поэтому я подумал, может быть, я мог бы использовать:

BigInteger.ToByteArray();

Хотя это работает очень быстро, я не мог понять, как сохранить его в читаемый текст.

Вы можете использовать описанный выше метод для записи двоичной строки в текстовый файл с этим самодельным расширением:

ToBinaryString

//Usage: string bigIntBinary = bigIntVar.ToBinaryString();
public static string ToBinaryString(this BigInteger source)
{
    //If you lookup the ToByteArray() method...
    //It actually stores the bytes in reverse order.
    var bigIntBytes = source.ToByteArray().Reverse();

    StringBuilder bigIntBinary = new StringBuilder();

    foreach (var bigIntByte in bigIntBytes)
    {
       bigIntBinary.Append(Convert.ToString(bigIntByte, 2).PadLeft(8, '0'));
    }

    return bigIntBinary.ToString();
}

ToBase64String

    ////Usage: string bigIntBase64 = bigIntVar.ToBase64String();
    public static string ToBase64String(this BigInteger source)
    {
        var bigIntBytes = source.ToByteArray().Reverse().ToArray();

        return Convert.ToBase64String(bigIntBytes);
    }

Я также попробовал математический способ (мод 10 и т. д.), чтобы получить каждую цифру, но это занимает ТОННУ больше времени, чем ToString().

Что я здесь делаю неправильно?


Этот код - это то, что я придумал на основе ответа ниже. Это быстрее, чем ToString(), но всего на пару секунд.

ToQuickString

//Usage: string bigIntString = bigIntVar.ToQuickString()
public static String ToQuickString(this BigInteger source)
{
    powersOfTen = new List<BigInteger>();

    powersOfTen.Add(1);

    for (BigInteger i = 10; i < source; i *= i)
    {
        powersOfTen.Add(i);
    }

    return BuildString(source, powersOfTen.Count - 1).ToString().TrimStart('0');
}

private static List<BigInteger> powersOfTen;

private static string BuildString(BigInteger n, int m)
{
    if (m == 0)
        return n.ToString();

    BigInteger remainder;
    BigInteger quotient = BigInteger.DivRem(n, powersOfTen[m], out remainder);

    return BuildString(quotient, m - 1) + BuildString(remainder, m - 1);
}

person Community    schedule 31.10.2012    source источник
comment
Кажется, что вызов ToString занимает довольно много времени, но если вы хотите, чтобы файл был удобочитаемым, вы мало что можете с этим поделать.   -  person Rawling    schedule 31.10.2012
comment
Да, я отредактировал, чтобы прояснить это. Я отлаживал его в течение некоторого времени и обнаружил, что ToString() и ничто другое вызывает задержку. Нет ли более быстрого способа превратить BigInteger в строку?   -  person user1787963    schedule 31.10.2012
comment
Я в этом сомневаюсь. Вам действительно нужно, чтобы они были понятными для человека? Никто не собирается сидеть и читать число из 450 тысяч символов, не так ли?   -  person Rawling    schedule 31.10.2012
comment
Ну, у меня есть текстовый файл размером 1 ГБ с 1 миллиардом цифр числа Пи ... Это было больше личное дело. Мне просто нравится знать, что у меня есть 450-тысячное число, которое что-то значит на моем компьютере.   -  person user1787963    schedule 31.10.2012
comment
Ха, честно :) Если бы для вас это означало то же самое, если бы файл был в удобочитаемом шестнадцатеричном формате, вызов .ToString("x") намного быстрее.   -  person Rawling    schedule 31.10.2012
comment
2 -> 4 -> 8 -> 6 -> 2.. двоичный код никогда не достигает 0 в десятичном виде, т.е. нет возможности оптимизировать его в десятичной системе счисления. Сохраните его в двоичном (или шестнадцатеричном (.ToString("X"))) формате :) Потому что никто не собирается его читать, верно?   -  person flindeberg    schedule 31.10.2012
comment
В вашем ToBinaryString удалите .ToList() после Reverse(). ToList() создает копию данных. Вам не нужна копия данных, ForEach может просто работать непосредственно с IEnumerable‹›, возвращаемым Reverse().   -  person dthorpe    schedule 03.11.2012


Ответы (2)


Сохраните данные BigInteger в двоичном или шестнадцатеричном формате. Он удобочитаем для компьютера и для достаточно преданных своему делу людей. ;>

Затрачивать дополнительные усилия на то, чтобы сделать вывод «удобочитаемым» — пустая трата времени. Ни один человек не сможет разобраться в 450 000 цифр, независимо от того, являются ли они основанием 10, основанием 16, основанием 2 или чем-то еще.

ОБНОВИТЬ

При более внимательном рассмотрении преобразования Base 10 можно заметить, что базовую производительность ToString можно сократить почти вдвое, используя несколько потоков в многоядерной системе. Основное препятствие заключается в том, что наибольший расход времени на весь процесс десятичной дроби составляет операция первого деления исходного числа из 450 000 разрядов.

Stats on my quad core P7: 
Generating a 500k digit random number using power and multiply: 5 seconds
Dividing that big number by anything just once: 11 seconds
ToString(): 22 seconds
ToQuickString: 18 seconds
ToStringMT: 12.9 seconds

.

public static class BigIntExtensions
{
    private static List<BigInteger> powersOfTen;

    // Must be called before ToStringMt()
    public static void InitPowersOfTen(BigInteger n)
    {
        powersOfTen = new List<BigInteger>();

        powersOfTen.Add(1);

        for (BigInteger i = 10; i < n; i *= i)
            powersOfTen.Add(i);
    }

    public static string ToStringMT(this BigInteger n)
    {
        // compute the index into the powersOfTen table for the given parameter. This is very fast.
        var m = (int)Math.Ceiling(Math.Log(BigInteger.Log10(n), 2));

        BigInteger r1;
        // the largest amount of execution time happens right here:
        BigInteger q1 = BigInteger.DivRem(n, BigIntExtensions.powersOfTen[m], out r1);

        // split the remaining work across 4 threads - 3 new threads plus the current thread
        var t1 = Task.Factory.StartNew<string>(() =>
        {
            BigInteger r1r2;
            BigInteger r1q2 = BigInteger.DivRem(r1, BigIntExtensions.powersOfTen[m - 1], out r1r2);
            var t2 = Task.Factory.StartNew<string>(() => BuildString(r1r2, m - 2));
            return BuildString(r1q2, m - 2) + t2.Result;
        });
        BigInteger q1r2;
        BigInteger q1q2 = BigInteger.DivRem(q1, BigIntExtensions.powersOfTen[m - 1], out q1r2);
        var t3 = Task.Factory.StartNew<string>(() => BuildString(q1r2, m - 2));
        var sb = new StringBuilder();
        sb.Append(BuildString(q1q2, m - 2));
        sb.Append(t3.Result);
        sb.Append(t1.Result);
        return sb.ToString();
    }

    // same as ToQuickString, but bails out before m == 0 to reduce call overhead.
    // BigInteger.ToString() is faster than DivRem for smallish numbers.
    private static string BuildString(BigInteger n, int m)
    {
        if (m <= 8)
            return n.ToString();

        BigInteger remainder;
        BigInteger quotient = BigInteger.DivRem(n, powersOfTen[m], out remainder);
        return BuildString(quotient, m - 1) + BuildString(remainder, m - 1);
    }
}

Для ToQuickString() и ToStringMT() перед использованием этих функций необходимо инициализировать массив степени 10. Инициализацию этого массива не следует включать в измерения времени выполнения функции, поскольку массив можно повторно использовать при последующих вызовах, поэтому стоимость его инициализации амортизируется в течение всего времени существования программы, а не вызовов отдельных функций.

Для производственной системы я бы установил более автоматическую инициализацию, такую ​​как инициализация разумного количества записей в статическом конструкторе класса, а затем проверка в ToQuickString() или ToStringMT(), чтобы увидеть, достаточно ли записей в таблице для обработки задано BigInteger. Если нет, добавьте в таблицу достаточно записей для обработки текущего BigInteger, а затем продолжите операцию.

Эта функция ToStringMT создает рабочие задачи вручную, чтобы распределить оставшуюся работу по 4 потокам на доступных исполнительных ядрах в многоядерном ЦП. Вместо этого вы могли бы просто заставить исходную функцию ToQuickString() выделять половину своей работы в другой поток при каждой рекурсии, но это быстро создает слишком много задач и увязает в накладных расходах планирования задач. Рекурсия проходит весь путь до отдельных десятичных цифр. Я изменил функцию BuildString(), чтобы она выходила раньше (m ‹= 8 вместо m == 0), потому что BigInteger.ToString() быстрее, чем DivRem для небольших чисел.

90% времени выполнения ToStringMt() занимает первый вызов DivRem. После этого он очень быстро сходится, но первый действительно болезненный.

person dthorpe    schedule 02.11.2012
comment
Я согласен, что при сохранении BigInteger лучше всего использовать Hex и Binary. Однако я искал более быстрый способ сохранить его в Base10. Я понимаю, что число из 450 тысяч цифр не будет полностью прочитано, но, тем не менее, я хотел сохранить файл в базе 10 (просто ради этого). - person user1787963; 02.11.2012
comment
Преобразование в Base10 требует много работы. Концептуально одно длинное деление (mod) для каждой цифры. 450k длинных делений займут некоторое время, особенно для данных BigInteger, где деление, вероятно, не реализовано аппаратно. Если производительность деления на 10 ^ n меньше n * (деление на 10), вы можете разбить BigInteger и распределить работу по десятичной дроби между несколькими потоками, чтобы воспользоваться преимуществами нескольких ядер выполнения. Может сократить время ToString() вдвое на 4-ядерном процессоре. Может очень хорошо работать на аппаратном обеспечении CUDA GPU с сотнями ядер. - person dthorpe; 03.11.2012
comment
Кроме того, поскольку вас действительно интересует только быстрое преобразование BigInteger с основанием 10, вы можете изменить заголовок вопроса или начать новый вопрос конкретно о преобразовании BigInteger и Base10 в строку. - person dthorpe; 03.11.2012

Сначала я бы вычислил все числа формы 10^(2^m) меньше, чем n. Затем я бы использовал DivRem с самой большой из них, чтобы разделить проблему на две подзадачи. Повторяйте это рекурсивно, пока не дойдете до отдельных цифр.

var powersOfTen=new List<BigInteger>();
powersOfTen.Add(1);
for(BigInteger i=10;i<n;i=i*i)
  powersOfTen.Add(i);

string ToString(BigInteger n, int m)
{
  if(m==0)
    return n.ToString();
  quotient = DivRem(n,powersOfTen[m], remainder)
  return ToString(quotient, m-1)+ToString(remainder, m-1)
}

Вы также можете полностью оптимизировать конкатенацию строк, напрямую записывая в массив символов.


В качестве альтернативы вы можете рассмотреть возможность использования базы 1000 000 000 во всех расчетах. Таким образом, вам вообще не нужно базовое преобразование. Это, вероятно, намного быстрее для факторного расчета.

List<int> multiply(List<int> f1, int f2)
{
  int carry=0;
  for(int i=0;i<f1.Count;i++)
  {
    var product=(Int64)f1[i]*(Int64)f2;
    carry=product/1000000000;
    result.Add(product%1000000000);
  }
  if(carry!=0)
    result.Add(carry);
}

Теперь преобразование в строку с основанием 10 тривиально и дешево.

person CodesInChaos    schedule 31.10.2012
comment
Ушел, чтобы добавить это в закладки и попробовать это в ближайшее время. Хотя сейчас нужно немного времени для сна. Мне очень нравится эта идея. Хотя рекурсия, вероятно, приведет к StackOverflow из-за 450 тыс. отдельных цифр. - person user1787963; 31.10.2012
comment
Нет, это не приведет к переполнению стека. Глубина рекурсии является логарифмической по количеству цифр. - person CodesInChaos; 31.10.2012
comment
Только что проверил этот код. Дайте мне знать, если я реализовал это неправильно. Я не думаю, что я сделал. Однако у меня не было времени сделать правильный код. Это была просто быстрая копия того, что вы сделали. - person user1787963; 02.11.2012
comment
Я просто хотел добавить, что в настоящее время он помещает все конечные нули на обоих концах числа. Возможно, я сделал что-то не так с кодом, лол. - person user1787963; 02.11.2012