Pull to refresh

Comments 48

Найдите логическую ошибку в коде выше.
while( i < j )
        {
            while ( i < j && vec[i].first + vec[j].first > e5 ) j--;
            if ( vec[i].first + vec[j].first == e5 )
            {
                bla bla = bla;
            }
            i++;
        }

Я правильно понимаю, что во втором случае условие выглядит так:
while ( 1 && vec[i].first + vec[j].first > e5 ) j--;

А ещё, не замедляет ли решение printf, как было в одной из предыдущих статей на эту тему?
Там же происходит j--, поэтому в процессе выполнения цикла условие i < j может нарушиться.
По хорошему, i < j надо вставить еще в следующий if, но когда это не выполняется, будет i=j и, в принципе, индекс массива j остается валидный.
Да, точно. Не увидел сразу, что весь цикл одной строчкой.
Ещё я не силён в порядке операций — сравнение на > e5 будет самым последним?
Почему используется &&, а не &?
Конкретно тут вроде неважно в каком порядке идут сравнения, но, скажем, в аналогичных циклах позже нужно будет сначала проверить, что j>=0, а только потом посмотреть что лежит в массиве по индексу j.

&& — это И для типа bool, & — логическое И для целочисленной маски.
Я опять валенок и перепутал :) Довольно непривычно видеть в условиях сразу два сравнения, да ещё и без скобок. Ну и перепутал && и &. Всё же && должно побыстрее быть.
Скажите, а есть примеры практического применения данного уравнения?
Насколько мне известно — нет.
У вас во вложенных циклах считаются gcd. Разве этот вспомогательный алгоритм не увеличивает сложность вычисления?
gcd там нужен чисто для отбраковки решений, в которых параметры имеют общий делитель. Поскольку решений, для которых нам нужно сделать такую проверку, очень мало, то сложность gcd можно не учитывать. Но если смотреть с формальной точки зрения — то да, надо везде домножить на логарифм. И решение #10 тогда получается со сложностью O(n5log n) (ouch!).
Существуют разные способы подсчета сложности. Если доказать, что общий делитель считается реже чем раз 1/log n, то он не внесет дополнительный множитель в алгоритм.
Ну, сейчас, вызовов этого gcd — O(n) с малой константой (около 1/144). Но доказать, что эта оценка справедлива при любом n, наверно, не проще, чем найти третье решение уравнения.
Неплохая работа, идея с дискретным корнем весьма интересна.

Еще у меня есть дикая идея проверить все n вплоть до миллиона. Ожидаемое время проверки, в принципе, реальное — около миллиона ядрочасов
Имхо, рано ещё, лучше потратить несколько часов (или десятков) на улучшение алгоритма и оптимизацию кода и сэкономить сотни тысяч часов обогрева комнаты на cpu (или gpu) ;)
В частности, рекомендую пользоваться gcc или clang — там есть встроенная поддержка __uint128_t.
С другой стороны — если не будет дополнительных ресурсов — то и оптимизировать особо смысла нет. Не думаю, что там можно более чем в 10 раз ускорить.

А так да, конечно, при возможной ресурсной поддержке — засяду оптимизировать насколько это возможно, опыт подобных расчетов есть (правда, «всего» на 55000 ядрочасов). Ну и не сразу миллион, а шажками: сначала 200000, потом 500000, а потом можно и за 1000000 взяться.
Не думаю, что там можно более чем в 10 раз ускорить.
Для 10000:

real 4m35.773s
user 4m33.321s
sys 0m2.459s

Думаю, у меня получится ещё разогнать.
1 ядро, 8ГБ памяти, алго O(N^3) по cpu и O(N^2) по памяти, по сути, аналог Вашего №5. Память — проблемное место, когда она заканчивается, то асимптотика по cpu выходит на O(N^4).
Ну так я свое #5 даже и не пытался ускорять дальше из-за того, что оно квадрат памяти требует. То есть насчет него — я согласен, что может и больше, чем в 10 раз ускорится. Ну а толку от него, если оно в память не лезет?
Эту версию ускорять уже смысла нет, согласен.
Надо работать над последней версией. Вполне может быть, что из-за маленького использования памяти она хорошо ляжет на GPU — ускорение будет в сотни раз.
Из дополнительных возможностей для оптимизации — сейчас множество вычислений повторяются многократно, их кешировать, хотя бы частично. Например, сохранить отсортированный список для каждого остатка, можно даже в файл.
Ну тогда я буду ускорять потихоньку. Сейчас вот экспериментировал в заменой сортировки с указателями на хэш-таблицу, и… получил двукратное ускорение и решение за O(N^3). Т.е. почти полностью вырезал все то время, который жрал лишний логарифм.
Пишут, что известны три решения:
Euler's conjecture was disproven by L. J. Lander and T. R. Parkin in 1966 when, through a direct computer search on a CDC 6600, they found a counterexample for k = 5. A total of three primitive (that is, in which the summands do not all have a common factor) counterexamples are known:

27^5 + 84^5 + 110^5 + 133^5 = 144^5 (Lander & Parkin, 1966),
(−220)^5 + 5027^5 + 6237^5 + 14068^5 = 14132^5 (Scher & Seidl, 1996), and
55^5 + 3183^5 + 28969^5 + 85282^5 = 85359^5 (Frye, 2004).
Совершенно верно, у меня совсем вылетело из головы указать, что я ищу решения только для a,b,c,d,e>0. Сейчас уточню это в статье.
Если «вывернуть на изнанку» циклы, можно получить сортированный вектор сразу на этапе инициализации.
В этом можно убедиться на примере однострочника на баше:
# с использованием сортировки
for a in $(seq 1 9); do for b in $(seq $(($a+1)) 10); do echo -n "$a^5+$b^5 = "; echo "$a^5+$b^5" | bc; done; done | sort -n -k3

# без использования сортировки
for a in $(seq 2 10); do for b in $(seq 1 $((a-1))); do echo -n "$a^5+$b^5 = "; echo "$a^5+$b^5" | bc; done; done

Я полагаю это может убавить заметное время выполнения.
Если я верно понял, но у Вас для n=20 в случае без сортировки после (a=19 b=18) будет идти (a=20 b=1). Но ведь 19^5+18^5 = 4365667 > 3200001 = 20^5+1^5. То есть, если для n=10 оно вроде работает, то потом, с ростом n, при переходе от (a (a-1)) к ((a+1) 1) получаем, что первое число будет где-то в 2 раза больше второго.
Да, я это обнаружил сразу после отправки комментария (об этом я отписался в личку, поскольку нет прав на редактирование).

Все же я провел некоторые тесты. В случае с «вывернутым» заполнением время сортировки уменьшается в разы, поскольку происходит меньше перестановок во время сортировки. Плюс немного уменьшил некоторое количество вычислений. С тестом можно ознакомиться по ссылке и рядом лежит файлик с результатами прогона:
https://github.com/theg4sh/experiments/blob/master/filling_loop_sortless.cpp
Это все действительно интересно и я, на самом деле, немного думал об ускорении сортировки в этом направлении. Если n достаточно мало, то массив действительно получается почти отсортированным и я пробовал его сортировать его вставками. К сожалению, с ростом n эта частичная отсортированность пропадает. Попробуйте померить вашим кодом случай, когда n порядка 10000.
Я провел тест и честно говоря ожидал увидеть цифры порядка нескольких минут. Сказать, что я был приятно удивлен — ничего не сказать. Смотрите сами:
repeats: 100
count: 10000
filling_origin x 100: 586.399347237
filling_minopt x 100: 585.032718914
filling_alloc x 100: 533.968311924
filling_lesssort x 100: 0.045311705
filling_lesssort_precalc x 100: 0.054538855
filling_lesssort_alloc x 100: 0.045214453

Я уменьшил количество прогонов для n=10000, но все же это какое-то… «безумие» :)
Насколько я понял, получается, что время заполнения вектора можно не учитывать при больших значениях n и это при одном потоке.

P/S Для сравнения, добавил результаты прогона для n=500 при 100 повторений в список. Так же поправил старые результаты из-за некорректного формата вывода времени.
Тут явно или что-то меряется, что-то сортится, или что-то выводится не так.
В разрыв по времени в 10 раз я может и поверю, но не в 10000 раз.
Ибо в каждом куске кода мы всегда как минимум создаем вектор за линейное время.
У вас в коде
for (int a=2; a<=n; a++) {
  for (int b=a-1; b<a; b++) {

ЭТО создает линейное число элементов вместо квадрата
Верно. Прошу прощения за невнимательность и спасибо, что нашли ошибку. Человеческий фактор, чтоб его.

Действительно, прирост в производительности небольшой:
filling_lesssort x 100: 552.314313408 1 iter avg 5.523143134
filling_lesssort_precalc x 100: 540.896136543 1 iter avg 5.408961365
filling_lesssort_alloc x 100: 497.780295379 1 iter avg 4.977802953

Я сделал некоторые оптимизации, связанные с пропущенным оператором break для условия (a5+b5<n5), логично предположить, что последующие итерации с увеличением b приведут к аналогичному результату.
Это дало ощутимый прирост:
filling_lesssort_alloc_break x 100: 159.098596131 1 iter avg 1.590985961

Код лежит там же на github`е
У вас filling_lesssort_alloc_break криво сумму считает:
sa5b5 будет равно a5 + 0 + 1 + 2^5 + 3^5 +… + b5
А нельзя было попытаться хардкорно оптимизировать №8?
Это имело бы смысл, если бы у меня была машина с 120Гб оперативной памяти.
Может я глупость скажу но нет ли смысла попробовать погонять алгоритм для чисел отличных от простых? Я просто на бумажке накидал: 27^5+84^5+110^5+133^5=144^5 если разложить числа на простые множители то получим:
27 = 3^3
84 = 2^2 * 3 * 7
110 = 2 * 5 * 7
133 = 7 * 19
144 = 2^4 * 3^2
т.е. все числа не простые. Аналогично я проверил для решения 55^5 + 3183^5 + 28969^5 + 85282^5 = 85359^5. Все числа так же раскладываются на простые множители.
Из этого я делаю «громкое» заключение что числа должны быть не простыми!
А теперь немного здравого смысла: если верить этому источнику, то от 1 до 3000 мы имеем 430 простых чисел — 14%. Итого эффективность алгоритма может возрасти примерно на 14 %))
Хотя сейчас начал проверять для 2682440^4 + 15365639^4 + 18796760^4 = 20615673^4.и обнаружил что 15365639 само является простое. Так что возможно моя теория потерпит фиаско.
Число простых с ростом n растет как n / ln n, т.е. для n=1000000 их уже всего всего 7%… Но если это утверждение хотя бы доказать — то тогда вполне можно добавить.
110 = 2 * 5 * 7
Это в какой алгебре?
С ростом N простые числа будут встречаться все реже и реже. На одних проверках простоты потеряем больше.
При этом нет никаких гарантий, что следующее решение не будет содержать простых чисел.
a^5+b^5 = (a+b)(a^4-a^3b+a^2b^2-ab^3+b^4)
Вдруг это поможет для нового метода :)
У нас есть два сервера для всяких расчетов. Один (Xeon E7-4850 — 4 CPUs, 40 cores, 128 Gb memory) сейчас свободен, поетому я решил поставить погонять эту программу.

С использованием OpenMP — 40 threads, 100% CPU:

image

#10:

n=20000 — 783 sec = чуть больше, чем 10 мин, т.е. примерно в 5 раз быстрее, чем в статье.

Я поставил для n=100000, завтра утром будет готово (у нас сейчас вечер)

У нас есть второй сервер (прям щас занят), в котором 48 «взрослых» cores и, кажется, 192 Гб.
Так, что если кому-то надо, то свистите, я могу на выходние поставить — кидайте код.

Интересно. 40 ядер старенького Westmere на 2,1 ГГц в 4,4 раза быстрее, чем 6 новых ядер на 3,4.
Это даже лучше, чем прирост мегагерцев (4,1раз)
Значит задача отлично паралелится и не оптимизирована под новые инструкции.
Хотелось бы уточнить у автора, на чем именно запускались 1 6 16 потоков (процессоры 8+12потоков)
Я запускал 20000 в 6 потоков на 4-ядерном i7-3770 3.4GHz (каждый поток замедляется из-за гипертрединга, но общее время улучшается). 50000 и 100000 я запускал так: 6 потоков на 4-ядерном i7-3770 3.4GHz, 10 потоков на 6-ядерном i7-5820K 3.3GHz.
Спасибо за предложение, я может до выходных еще оптимизаций на #10 повешу.
Я ничего не оптимизировал. Просто взял #10 код, вставил во внешнем цикле "#pragma omp parallel for" и выделил память под массивы vec1, vec2 внутри цикла
компилил MSVC12, GCC 5.3
Никаких принципиальных отличий MS — GCC не нашел

будет новый вариант — свистите громко. Хорошо бы до НГ, мы гуляем 3 дня, на работу 2-го.
После этого — придется гонять по ночам, серверы используются днем
Я постараюсь выкатить новый вариант сегодня. Я перепробовал по-отдельности кучу идей, несколько из них дали ускорение, теперь нужно просто объединить их вместе в одном решении. Ожидается ускорение в 3-4 раза.
для 100 тыщ время было около 20 часов, посколку сервер был занят основной работой. Время процессора я не подумал пропечатать, только полное время

Второй сервер попроще, Xeon E5 2690, 2.9 GHz, 32 cores
зато памяти побольше — 256 ГБ
можно раскидать между двумя кампутерами
Верно, (27*7943)^5 + (84*7943) ^5 + (110*7943)^5 + (133*7943)^5 = (144*7943)^5

Да и вообще, (27*X)^5 + (84*X) ^5 + (110*X)^5 + (133*X)^5 = (144*X)^5 для любого X>0…
Артем, спасибо за текст. Вдохновляет. А гипотезу Била уже решили? Там хотя бы миллион можно получить)
Про гипотезу Била не в курсе как там с ней сейчас.
Sign up to leave a comment.

Articles