@Zxvvo

Почему эта программа вычисляет факториал больших чисел неправильно?

Крайне интересная программка, призванная вычислять факториал любого числа, вплоть до миллиона и больше. Но есть один нюанс - если взять небольшое число, там однозначное или двухзначное, она успешно его посчитает, мы можем свериться с интернетом и всё верно. Но например факториал 1000 или других больших чисел - число получается неправильным. В чём причина такого феномена и как это исправить?

Так-же хотелось бы услышать ваше мнение о программе, и как её можно ещё оптимизировать для ещё более быстрого расчёта?

#include <vector>
#include <iostream>
#include <thread>
#include <mutex>
#include <fstream>
#include <sstream>
#include <iomanip>
constexpr unsigned BASE = 1000000000;
std::mutex mtx;
void multiply_range(std::vector<unsigned long long>& factorial, unsigned long long start, unsigned long long end) {
    unsigned long long carry = 0;
    for (unsigned long long i = start; i <= end; ++i) {
        for (size_t j = 0; j < factorial.size(); ++j) {
            unsigned long long result = factorial[j] * i + carry;
            factorial[j] = result % BASE;
            carry = result / BASE;
        }
        while (carry) {
            std::lock_guard<std::mutex> lock(mtx);
            factorial.push_back(carry % BASE);
            carry /= BASE;
        }
    }
}
std::vector<unsigned long long> fast_factorial(unsigned long long& n) {
    std::vector<unsigned long long> factorial;
    factorial.reserve(1 + n);
    factorial.push_back(1);
    unsigned num_threads = std::thread::hardware_concurrency();
    std::vector<std::thread> threads;
    unsigned long long range = n / num_threads;
    for (unsigned i = 0; i < num_threads; ++i) {
        unsigned long long start = i * range + 2;
        unsigned long long end = (i == num_threads - 1) ? n : (start + range - 1);
        threads.emplace_back(multiply_range, std::ref(factorial), start, end);
    }
    for (auto& thread : threads) {
        thread.join();
    }
    return factorial;
}
void write_factorial_to_file(unsigned long long& n, const std::vector<unsigned long long>& factorial) {
    std::ostringstream filename;
    filename << "Факториал числа " << n << ".txt";
    std::ofstream outfile(filename.str());
    if (outfile.is_open()) {
        for (int i = factorial.size() - 1; i >= 0; i--) {
            if (i != factorial.size() - 1) {
                outfile << std::setw(9) << std::setfill('0') << factorial[i];
            }
            else {
                outfile << factorial[i];
            }
        }
        outfile << std::endl;
        outfile.close();
    }
    else {
        printf("Не удалось открыть файл для записи.");
        printf("\n");
        printf("Попробуйте переместить программу в другую папку.");
        return;
    }
}
int main() {
    setlocale(LC_ALL, "ru");
    unsigned long long n;
    while (true) {
        printf("Введите число, для которого нужно вычислить факториал: ");
        std::cin >> n;
        while (n <= 0) {
            printf("Некорректное число, введите другое число: ");
            std::cin >> n;
        }
        printf("Вычисление факториала...\n");
        std::vector<unsigned long long> factorial = fast_factorial(n);
        write_factorial_to_file(n, factorial);
        printf("Результат записан в файл: Факториал числа %llu.txt\n\n", n);
    }
}
  • Вопрос задан
  • 292 просмотра
Решения вопроса 3
@SunTechnik
Так как в первоначальной версии ответа наехал не по теме, но в комментах люди дали советы, ответ тереть не буду...

Для записи факториала числа 1000 потребуется 2 568 цифр
https://zeptomath.com/calculators/factorial.php?nu...

В программе реализован свой вариант длинной математики.
По результатам тестов - проблема в распараллеливании... (Идею подсказал Adamos )
Ответ написан
@res2001
Developer, ex-admin
Видимо ошибка в длинных вычислениях. Сложно просто глядя на код что-то сказать.
Для оптимизации тут применена многопоточность, но похоже, что присутствуют гонки, хотя есть попытка использовать мьютекс для защиты, но явно этого не достаточно.
Похоже, что эта программа толком не отлаживалась.

Думаю, больше толку было бы для оптимизации, если бы использовались SIMD инструкции, а не многопоточность.
Есть оптимизированные библиотеки для длинной арифметики, проще всего было бы использовать какую-то подобную библиотеку, а не изобретать собственный велосипед. С использованием такой библиотеки задача была бы достаточно тривиальной.
Ответ написан
wataru
@wataru Куратор тега C++
Разработчик на С++, экс-олимпиадник.
Полная фигня с распараллеливанием. Ну нельзя длинное число параллельно умножать сразу на много чисел. Во-первых, там гонка данных. Один поток может прочитать цифру, домножить ее на текущее число, в это время другой поток запишет свой результат, и тут первый поток этот результат перезапишет. Результат работы одного из потоков просто потерян. Но даже без этого - каждый поток должен видеть все цифры числа как они были, когда он начал умножать. Если какой-то другой поток там хоть что-то запишет - у вас бред будет.

Правильное распараллеливание будет в умножении на одно число. Каждый поток домножает на число свой промежуток цифр и делает перносы только внутри своего отрезка цифр. Потом все потоки спят, а один делает переносы между границами отрезков и расширяет вектор, делая переносы в конце. При этом у вас в числе какие-то разряды будут больше базы. Потом потоки продолжают умножать уже на следующее число. Надо их синхронизировать какими-то заборами/событиями между каждым новым числом. В конце придется нормализовать число в один поток. Возможно это можно тоже распараллелить, но это сложно будет совсем.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
mayton2019
@mayton2019
Bigdata Engineer
По поводу факториала 1000 и более толстых факториалов.
Есть формула Стирлинга. Она позволяет приблизительно прикинуть
число в вещественной форме.

Тебе это надо чтобы ответить на вопрос, а вообще принципиально возможно ли посчитать это
число и положить его в память.
Например для 1000 последние члены этого ряда в умножении будут
добавлять по 3 десятичных разряда.

.......998 * 998 * 999 =

Должно быть порядка 2500 знаков.

По поводу AVX и прочее. Один регистр AVX будет брать 512 бит. В перерасчете на десятичную
систему - можно делить грубо на 3 и это будет 170 десятичных цифр. Тоесть надо много
таких регистров.

По поводу этого кода.

unsigned long long result = factorial[j] * i + carry;
            factorial[j] = result % BASE;
            carry = result / BASE;


Я не понимаю что здесь происходит. Зачем ты переводишь в систему счисления с основнием 1000000000

Двоичная система нормально работает. Используй ее возможности. А это - какой-то рудимент.


Я просто не программист, это у меня задание такое.
Сторонние библиотеки по условиям нельзя.

Про SIMD слышу впервые.

Это вообще приводит в изумление. По хорошему, мультипоточка и параллелизм это очень
сложная тема. Очень синьорная и в ней много всяких квантовых эффектов. И чтобы ей
просто заниматься нужно иметь хотябы 1-2 года опыта. Сомнительно чтобы новичек
вообще что то написал (и отладил под нагрузкой) работающее. Или разве что тебе
ChatGPT что-то там написал и ты просто случайно где-то проскочил. Повезло наверное.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы