Что лучше умножить на 2 или прибавить число к самому себе? BIGnums

Мне нужна помощь, чтобы решить, что лучше производительности. Я работаю с bigint (более 5 миллионов цифр), и большая часть вычислений (если не все) приходится на удвоение текущего bigint. Итак, я хотел знать, лучше ли умножить каждую ячейку (часть bigint) на 2, а затем изменить ее, а остальное вы знаете. Или лучше просто добавить значение bigint к самому себе.

Я тоже немного думаю о простоте реализации (сложение двух бигинт сложнее, чем умножение на 2), но меня больше беспокоит производительность, а не размер кода или простота реализации.

Дополнительная информация: Я запрограммирую это на C ++, я хорошо знаком с bigints (просто никогда не сталкивался с этой проблемой). Мне не нужен какой-либо исходный код или что-то подобное, мне просто нужно хорошее мнение и объяснение / доказательство этого, так как мне нужно принять хорошее решение с самого начала, поскольку проект будет довольно большим и в основном построен вокруг этой части это сильно зависит от того, что я выбрал сейчас.

Спасибо.


person Krunch    schedule 11.08.2009    source источник
comment
Вы должны проверить этот вопрос: stackoverflow.com/questions/235072/ Граничит с дубликатом ...   -  person Brian Rasmussen    schedule 11.08.2009
comment
У вас ведь есть профайлер? Это твой ответ.   -  person plinth    schedule 11.08.2009
comment
Какую разницу в производительности вы видите? Во-первых, протестируйте его.   -  person mpez0    schedule 11.08.2009
comment
Что ж, я должен сначала проверить это, как я уже сказал, я только начал вычислять, так что источника еще нет. Но будьте уверены, я протестирую оба варианта, но все же ответы предоставили отличную информацию для будущих проектов. Скорее всего, я опубликую здесь результаты теста после того, как напишу код.   -  person Krunch    schedule 11.08.2009
comment
Я заметил, что вы уже выбрали лучший ответ на этот вопрос, но, возможно, вы захотите взглянуть на мой… Я думаю, что вы, возможно, полностью лаете не на то дерево.   -  person derobert    schedule 11.08.2009
comment
Ну, я оставил комментарий, почему я не могу просто хранить его в двоичном формате и работать с ним, поскольку мне нужно, чтобы он оставался в базе 10.   -  person Krunch    schedule 11.08.2009
comment
@Krunch: спасибо, обновил свой ответ. Мне действительно любопытно, что, черт возьми, вы сейчас вычисляете :-)   -  person derobert    schedule 12.08.2009


Ответы (7)


Попробуйте сдвигать каждый бит. Это, наверное, самый быстрый способ. Когда вы сдвигаете целое число влево, вы удваиваете его (умножаете на 2). Если у вас есть несколько длинных целых чисел в цепочке, вам нужно сохранить самый старший бит, потому что после его сдвига он исчезнет, ​​и вам нужно использовать его как младший бит для следующего длинного целого числа.

На самом деле это не имеет большого значения. Современные 64-битные компьютеры могут складывать два целых числа за одно и то же время, необходимое для их сдвига (1 тактовый цикл), поэтому это займет столько же времени. Я предлагаю вам попробовать разные методы, а затем сообщить, если есть какие-либо существенные различия во времени. Все три метода должны быть простыми в реализации, и создание числа размером 5 МБ также должно быть простым с использованием генератора случайных чисел.

person Marius    schedule 11.08.2009

Чтобы сохранить целое число из 5 миллионов цифр, вам понадобится довольно много бит - 5 миллионов, если вы имели в виду двоичные цифры, или ~ 17 миллионов бит, если это были десятичные цифры. Предположим, что числа хранятся в двоичном представлении, а арифметические операции выполняются кусками определенного размера, например 32 бита или 64 бита.

  • При добавлении числа к самому себе, каждый фрагмент добавляется к самому себе и к переносу от добавления предыдущего фрагмента. Любой перенос сохраняется для следующего фрагмента. Это пара дополнительных операций и небольшая бухгалтерия для отслеживания переноса.

  • Если умножение на два с помощью сдвига влево, это одна операция сдвига влево для умножения и одна операция сдвига вправо + и с 1 для получения переноса. Вести бухгалтерский учет немного проще.

Внешне сдвижная версия выглядит немного быстрее. Однако общая стоимость удвоения числа во многом зависит от его размера. Число в 17 миллионов бит превышает кэш L1 ЦП, и время обработки, вероятно, перегружено операциями выборки из памяти. На современном оборудовании ПК выборка из памяти на несколько порядков медленнее, чем сложение и смещение.

При этом вы можете выбрать тот, который вам проще реализовать. Я склоняюсь к версии с левым переключением.

person Oren Trutner    schedule 11.08.2009
comment
Мне хорошо известно об узком месте передачи памяти, но мне придется смириться с этим, поскольку можно ожидать, что число со временем вырастет более чем на 10 миллионов цифр, но это еще не все. - person Krunch; 11.08.2009
comment
Ключевым моментом является то, что ЦП ограничен выборкой памяти, а не обработкой. Неважно, добавляется он или смещается, и какие оптимизации делает компилятор, потому что во времени преобладает ожидание завершения транзакций с памятью. - person Oren Trutner; 11.08.2009
comment
Верно, но между выборками из памяти (во время фактической обработки) я могу заставить ее работать меньше, поэтому сокращаю время обработки и тем самым сокращаю общее время. - person Krunch; 11.08.2009
comment
Предполагается, что предварительная выборка, выполнение вне очереди и т. Д. Должны сделать так, чтобы не было точки, в которой он не ждал в памяти. - person derobert; 12.08.2009

ты пробовал сдвигать биты?
‹---------------- умножается на 2
›› делится на 2

person Robert Greiner    schedule 11.08.2009
comment
Эм, ‹< - умножить, ›› - делить;) - person Marcin Deptuła; 11.08.2009
comment
Что ж, я знаю об этом, и на самом деле большинство современных компиляторов C ++ при компиляции исходного кода обмениваются * 2 и / 2 с ›› 1 и ‹front 1. Так что это не совсем улучшение. И вы, и Мариус упустили суть вопроса, так как я хотел знать, эффективнее ли прибавить bigint к самому себе или лучше умножить bigint на 2. - person Krunch; 11.08.2009

Сдвиг левого бита на единицу аналогичен умножению на два!
Эта ссылка Разъясняет механизм и приводит примеры.

int A = 10; //...01010 = 10
int B = A<<1; //..010100 = 20
person Matthieu    schedule 11.08.2009

Если это действительно важно, вам нужно написать все три метода (включая битовый сдвиг!) И профилировать их для различных входных данных. (Используйте маленькие числа, большие числа и случайные числа, чтобы избежать искажения результатов.)

Извините за ответ «Сделай сам», но это действительно лучший способ. Никто не заботится об этом результате больше, чем вы, поэтому вы лучше всех его поймете.

person ojrac    schedule 11.08.2009
comment
Я спросил это, потому что я думал, что кто-то уже это сделал, но это не так. Все происходит впервые, так что, похоже, я должен найти правду. - person Krunch; 11.08.2009
comment
В общем, профилирование очень зависит от среды. Чьи-то результаты годичной давности, вероятно, не подходят вам, вашему компилятору и оптимизации, которую вы используете. - person ojrac; 11.08.2009

Хорошо реализованное умножение BigNums составляет O (N log (N) log (log (N )). Сложение - O (n). Следовательно, сложение самого себя должно быть быстрее, чем умножение на два. Однако это верно только в том случае, если вы умножаете два произвольных больших числа; если ваша библиотека знает, что вы умножаете большое число на небольшое целое он может быть оптимизирован до O (n).

Как отмечали другие, битовый сдвиг также возможен. Это также должно быть O (n), но более быстрое постоянное время. Но это будет работать только в том случае, если ваша библиотека bignum поддерживает битовый сдвиг.

person Nelson    schedule 11.08.2009
comment
Я сам все кодирую, поэтому могу добавить бесчисленное количество функций, чтобы проверить это. Как я уже говорил в некоторых предыдущих комментариях, я опубликую некоторые результаты тестирования после того, как закодирую несколько версий. - person Krunch; 11.08.2009

большая часть вычислений (если не все) приходится на удвоение текущего bigint

Если все ваши вычисления заключаются в удвоении числа, почему бы вам просто не сохранить отдельное поле шкалы (с основанием 2)? Затем просто добавьте один к scale, который может быть просто старым int. Это наверняка будет быстрее, чем любые манипуляции с каким-то нечетным миллионом бит.

IOW, используйте bigfloat.

случайный тест

use Math::GMP;
use Time::HiRes qw(clock_gettime CLOCK_REALTIME CLOCK_PROCESS_CPUTIME_ID);

my $n = Math::GMP->new(2);
$n = $n ** 1_000_000;

my $m = Math::GMP->new(2);
$m = $m ** 10_000;

my $str;
for ($bits = 1_000_000; $bits <= 2_000_000; $bits += 10_000) {
    my $start = clock_gettime(CLOCK_PROCESS_CPUTIME_ID);
    $str = "$n" for (1..3);
    my $stop = clock_gettime(CLOCK_PROCESS_CPUTIME_ID);
    print "$bits,@{[($stop-$start)/3]}\n";
    $n = $n * $m;
}

Кажется, это показывает, что каким-то образом GMP выполняет преобразование за время O (n) (где n - количество бит в двоичном числе). Это может быть связано с особым случаем наличия единицы, за которой следует миллион (или два) нулей; в документах GNU MP говорится, что он должен быть медленнее ( но все же лучше, чем O (N ^ 2).

http://img197.imageshack.us/img197/6527/chartp.png

person derobert    schedule 11.08.2009
comment
Да, это была моя первая идея, но я должен уметь записать ее в базе 10 и записать в файл, так что представьте, что мне нужно вычислить 2 ^ 1000000, затем 2 ^ 999999 и так далее, это одна большая проблема (ну немного меньше, если вы сделаете это в обратном порядке, например 2 ^ 0, затем 2 ^ 1), но это все еще гигантская задача, и вычисление 2 ^ 1000000 приходит к той же проблеме, которую я тогда задавал, то есть мне нужно удваивать bigint бесчисленное количество раз . - person Krunch; 11.08.2009
comment
GMP может преобразовать 2 ^ 1000000 в десятичную дробь за 0,2 секунды на моей относительно медленной (P4) машине. Сколько из них вы выводите ?! - person derobert; 12.08.2009