Теперь, когда все разработали свой (часто удивительный) опыт низкоуровневого кодирования для « Как медленно работает Python?» (Или как быстро ваш язык?) И как медленно работает Python (часть II)?пришло время для испытания, которое также расширит вашу способность улучшать алгоритм.
Следующий код вычисляет список длины 9. Позиция i
в списке подсчитывает количество раз, i
когда были найдены хотя бы последовательные нули при вычислении внутренних произведений между F
и S
. Чтобы сделать это точно, он перебирает все возможные списки F
длины n
и списки S
длины n+m-1
.
#!/usr/bin/python
import itertools
import operator
n=8
m=n+1
leadingzerocounts = [0]*m
for S in itertools.product([-1,1], repeat = n+m-1):
for F in itertools.product([-1,1], repeat = n):
i = 0
while (i<m and sum(map(operator.mul, F, S[i:i+n])) == 0):
leadingzerocounts[i] +=1
i+=1
print leadingzerocounts
Выход
[4587520, 1254400, 347648, 95488, 27264, 9536, 4512, 2128, 1064]
Если вы увеличите n до 10,12,14,16,18,20 с помощью этого кода, он очень быстро станет слишком медленным.
правила
- Задача состоит в том, чтобы дать правильный вывод для максимально большого числа n. Только четные значения n актуальны.
- Если есть ничья, выигрыш переходит к самому быстрому коду на моей машине для наибольшего n.
- Я оставляю за собой право не тестировать код, который занимает более 10 минут.
- Вы можете изменить алгоритм любым удобным вам способом, если он дает правильный результат. На самом деле вам придется изменить алгоритм, чтобы добиться приличного прогресса на пути к победе.
- Победитель будет награжден через неделю после того, как был задан вопрос.
- Награда будет присуждена в установленный срок, то есть немного позже, когда победитель будет награжден.
Моя машина Время будет запущено на моей машине. Это стандартная установка Ubuntu на восьмиъядерный процессор AMD FX-8350. Это также означает, что мне нужно иметь возможность запускать ваш код. Как следствие, используйте только легкодоступное бесплатное программное обеспечение и, пожалуйста, включите подробные инструкции по компиляции и запуску вашего кода.
Статус .
- C . n = 12 за 49 секунд @Fors
- Java . n = 16 в 3:07 от @PeterTaylor
- С ++ . n = 16 в 2:21 @ilmale
- Rpython . n = 22 в 3:11 @primo
- Java . n = 22 в 6:56 @PeterTaylor
- Нимрод . n = 24 за 9:28 секунд от @ReimerBehrends
Победителем стал Реймер Берендс с записью в Nimrod!
В качестве проверки вывод для n = 22 должен быть [12410090985684467712, 2087229562810269696, 351473149499408384, 59178309967151104, 9975110458933248, 1682628717576192, 284866824372224, 48558946385920, 8416739196928, 1518499004416, 301448822784, 71620493312, 22100246528, 8676573184, 3897278464, 1860960256, 911646720, 451520512, 224785920, 112198656, 56062720, 28031360, 14015680]
.
Конкурс завершен, но ... Я буду предлагать 200 баллов за каждую заявку, которая увеличивается n на 2 (в течение 10 минут на моем компьютере), пока у меня не кончатся баллы. Это предложение открыто навсегда .
источник
Ответы:
Нимрод (N = 22)
Компилировать с
(Нимрод можно скачать здесь .)
Это выполняется за отведенное время для n = 20 (и для n = 18, когда используется только один поток, в последнем случае это занимает около 2 минут).
Алгоритм использует рекурсивный поиск, сокращая дерево поиска всякий раз, когда встречается ненулевой внутренний продукт. Мы также сократили пространство поиска пополам, наблюдая, что для любой пары векторов
(F, -F)
нам нужно рассмотреть только один, потому что другой производит те же самые наборы внутренних произведений (отрицаяS
).Реализация использует средства метапрограммирования Nimrod, чтобы развернуть / встроить первые несколько уровней рекурсивного поиска. Это экономит немного времени при использовании gcc 4.8 и 4.9 в качестве бэкэнда Nimrod и достаточное количество для clang.
Пространство поиска может быть дополнительно сокращено, если заметить, что нам нужно учитывать только значения S, которые отличаются четным числом первых N позиций от нашего выбора F. Однако сложность или потребности в памяти не масштабируются для больших значений N, учитывая, что тело цикла полностью пропускается в этих случаях.
Табулирование, где внутренний продукт равен нулю, оказывается быстрее, чем использование любых функций подсчета битов в цикле. Видимо доступ к таблице имеет довольно хорошую местность.
Похоже, что проблема должна быть поддающейся динамическому программированию, учитывая, как работает рекурсивный поиск, но нет очевидного способа сделать это с разумным объемом памяти.
Пример выходов:
N = 16:
N = 18:
N = 20:
В целях сравнения алгоритма с другими реализациями, N = 16 занимает около 7,9 секунды на моем компьютере при использовании одного потока и 2,3 секунды при использовании четырех ядер.
N = 22 занимает около 15 минут на 64-ядерном компьютере с gcc 4.4.6 в качестве бэкэнда Nimrod и переполняет 64-битные целые числа
leadingZeros[0]
(возможно, не беззнаковые, не смотрели на это).Обновление: я нашел место для еще пары улучшений. Во-первых, для заданного значения
F
мы можемS
точно перечислить первые 16 записей соответствующих векторов, потому что они должны точно отличаться в разныхN/2
местах. Таким образом, мы предварительно вычисляем список битовых векторов размераN
, в которыхN/2
установлены биты, и используем их для получения начальной частиS
изF
.Во-вторых, мы можем улучшить рекурсивный поиск, наблюдая, что мы всегда знаем значение
F[N]
(поскольку MSB равен нулю в битовом представлении). Это позволяет нам точно предсказать, в какую ветвь мы перейдем из внутреннего продукта. Хотя это на самом деле позволило бы нам превратить весь поиск в рекурсивный цикл, на самом деле это совсем немного мешает предсказанию ветвлений, поэтому мы сохраняем верхние уровни в первоначальном виде. Мы все еще экономим некоторое время, прежде всего, за счет уменьшения количества ветвлений, которые мы делаем.Для некоторой очистки код теперь использует целые числа без знака и фиксирует их на 64-битном (на тот случай, если кто-то захочет запустить это на 32-битной архитектуре).
Общее ускорение составляет от х3 до х4. Для N = 22 по-прежнему требуется более восьми ядер для работы менее чем за 10 минут, но на 64-ядерном компьютере это теперь сокращается примерно до четырех минут (с
numThreads
увеличением). Я не думаю, что есть гораздо больше возможностей для улучшения без другого алгоритма.N = 22:
Обновлен снова, используя дальнейшие возможные сокращения в пространстве поиска. Работает примерно 9:49 минут для N = 22 на моей четырехъядерной машине.
Окончательное обновление (я думаю). Классы лучшей эквивалентности для выбора F, сокращая время выполнения для N = 22 до
3:19 минут57 секунд (правка: я случайно запустил это только с одним потоком) на моей машине.Это изменение использует тот факт, что пара векторов производит одинаковые ведущие нули, если один можно преобразовать в другой, вращая его. К сожалению, довольно критичная низкоуровневая оптимизация требует, чтобы верхний бит F в битовом представлении всегда был одинаковым, и при использовании этой эквивалентности значительно сократил пространство поиска и сократил время выполнения примерно на четверть по сравнению с использованием другого пространства состояний сокращение на F, издержки от устранения низкоуровневой оптимизации более чем компенсируют ее. Однако оказывается, что эту проблему можно устранить, если учесть тот факт, что F, обратные друг другу, также эквивалентны. Хотя это немного усложнило вычисление классов эквивалентности, оно также позволило мне сохранить вышеупомянутую низкоуровневую оптимизацию, что привело к ускорению примерно в 3 раза.
Еще одно обновление для поддержки 128-битных целых чисел для накопленных данных. Для компиляции с 128 - битными целыми числами, вам нужно
longint.nim
от сюда и компилировать с-d:use128bit
. N = 24 по-прежнему занимает более 10 минут, но я включил приведенный ниже результат для интересующихся.N = 24:
источник
Java (
n=22
?)Я думаю, что большинство ответов лучше, чем
n=16
использование аналогичного подхода к этому, хотя они отличаются по симметрии, которую они используют, и по тому, как они делят задачу между потоками.Векторы, определенные в вопросе, могут быть заменены битовыми строками, а внутренний продукт - XOR, перекрывающим перекрывающее окно и проверяющим, установлены ли именно
n/2
биты (и, следовательно,n/2
биты очищены). Существуютn! / ((n/2)!)
цепочкиn
битов (центральный биномиальный коэффициент) сn/2
установленными битами (которые я называю сбалансированными строками), поэтому для любого заданного числаF
существует множество окон,S
которые дают нулевой внутренний продукт. Более того, действие скольженияS
вдоль единицы и проверки, можем ли мы все еще найти входящий бит, который дает нулевой внутренний продукт, соответствует поиску ребра в графе, узлами которого являются окна, а ребра которого связывают узелu
с узлом,v
чьи первыеn-1
биты последниеn-1
битыu
.Например, с
n=6
иF=001001
мы получаем этот график:и для
F=001011
мы получаем этот график:Затем нам нужно рассчитывать для каждого
i
из0
кn
сколько путей длиныi
есть, суммируя по графикам на каждыйF
. Я думаю, что большинство из нас используют поиск в глубину.Обратите внимание, что графики разрежены: легко доказать, что каждый узел имеет степень не более 1, а степень не более 1. Это также означает, что единственными возможными структурами являются простые цепочки и простые петли. Это немного упрощает DFS.
Я использую пару симметрий: уравновешенные строки закрываются под инвертированными битами (
~
операция на многих языках из семейства ALGOL) и при ротации битов, поэтому мы можем сгруппировать значения,F
которые связаны этими операциями, и выполнять только DFS один раз.На моем 2.5 ГГц Core 2 я получаю
Поскольку компьютер Лембика имеет 8 ядер и выполнил мою предыдущую однопоточную программу вдвое быстрее моей, я уверен, что она будет выполнена
n=22
менее чем за 8 минут.источник
С
Это просто слегка оптимизированная реализация алгоритма в вопросе. Это может обойтись
n=12
в срок.Тестовый прогон
n=12
, включая компиляцию:Комментарий: я просто включил свой мозг и использовал простую комбинаторику, чтобы вычислить, что первое значение всегда будет
n! / ((n / 2)!)^2 * 2^(n + m - 1)
. Мне кажется, что должно быть полностью алгебраическое решение этой проблемы.источник
Джава,
n=16
Для любого заданного значения
F
существуют\binom{n}{n/2}
векторы, которые имеют нулевое внутреннее произведение. Таким образом, мы можем построить граф, вершинами которого являются те совпадающие векторы, а ребра которого соответствуют смещениюS
, и тогда нам просто нужно посчитать пути длиныn
в графе.Я не пробовал микрооптимизировать это, заменяя условные выражения побитовыми операциями, но каждое двойное увеличение
n
увеличивает время выполнения примерно в 16 раз, так что это не будет иметь большого значения, если я не достаточно близок к порогу. На моей машине я нет.На моем 2.5 ГГц Core 2 я получаю
источник
f
и начальным вершинам итерируйте по всемf_xor_g
с точноn/2
установленными битами. Для каждого из них, переберите всеf
и возьмитеg = f ^ f_xor_g
.RPython, N = 22 ~ 3: 23
Многопоточный, с использованием рекурсивного спуска без стеков. Программа принимает два аргумента командной строки: N и количество рабочих потоков.
Скомпилировать
Создайте локальный клон PyPy-репозитория, используя mercurial, git или что угодно. Введите следующее заклинание (при условии, что вышеупомянутый скрипт назван
convolution-high.py
):где
%PYPY_REPO%
представляет переменную среды, указывающую на репозиторий, который вы только что клонировали. Компиляция занимает около минуты.Пример времени
N = 16, 4 темы:
N = 18, 4 темы:
N = 20, 4 темы:
N = 22, 4 темы:
источник
Python 3.3, N = 20, 3,5 мин
Отказ от ответственности: мое намерение НЕ размещать это как мой собственный ответ, так как алгоритм я использую только бесстыдный порт из раствора RPython Primo в . Моя цель здесь - показать, что вы можете сделать в Python, если сочетаете магию Numpy и модулей Numba .
Нумба объяснил вкратце:
Обновление 1 : я заметил после бросания чисел, что мы можем просто полностью пропустить некоторые числа. Так что теперь MaxF становится (1 << n) // 2, а maxs становится maxf 2 **. Это немного ускорит процесс. n = 16 теперь занимает всего ~ 48 секунд (вместо 4,5 минут). У меня также есть другая идея, которую я собираюсь попробовать и посмотреть, смогу ли я сделать это немного быстрее.
Обновление 2: изменен алгоритм (решение primo). Хотя мой порт еще не поддерживает многопоточность, добавить его довольно просто. Можно даже выпустить CPython GIL, используя Numba и ctypes. Однако это решение работает очень быстро и на одном ядре!
И наконец:
Это работает на моей машине в 212688 мс или ~ 3,5 мин.
источник
C ++ N = 16
Я тестирую EEEPC с атомом ... мое время не имеет большого смысла. : D
Атом решает n = 14 за 34 секунды. И n = 16 через 20 минут. Я хочу проверить n = 16 на ПК. Я настроен оптимистично
Идея состоит в том, что каждый раз, когда мы находим решение для данного F, мы находим 2 ^ i решение, потому что мы можем изменить нижнюю часть S, приводя к тому же результату.
Скомпилировать:
gcc 26459.cpp -std = c ++ 11 -O3 -march = собственный -fstrict-aliasing -ftree-vectorize -Wall -pedantic -o 26459
источник
JAVASCRIPT n: 12
В моем компьютере это заняло 231,242 секунды. В демоверсии я использую веб-работников для предотвращения зависания браузера. Это наверняка может быть улучшено с параллельными работниками. Я знаю, что у JS нет шансов в этом испытании, но я сделал это для удовольствия!
Нажмите, чтобы запустить онлайн демо
источник
n=22
[235388928,86292480,19031048,5020640,1657928,783920,545408,481256,463832,460256,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744]
i.imgur.com/FIJa2Ch.pngФортран: n = 12
Я только что сделал быструю и грязную версию на Фортране, без оптимизаций, кроме OpenMP. Он должен выжать чуть меньше 10 минут для n = 12 на машине OP, это займет 10:39 на моей машине, что немного медленнее.
64-битные целые действительно оказывают негативное влияние на производительность; думаю, мне придется переосмыслить весь алгоритм, чтобы это было намного быстрее. Не знаю, буду ли я беспокоиться, думаю, я скорее потрачу некоторое время на то, чтобы придумать хороший вызов, который больше соответствует моим собственным вкусам. Если кто-то еще хочет взять это и бежать с этим, продолжайте :)
источник
Луа: n = 16
Отказ от ответственности: я не собираюсь публиковать это как мой собственный ответ, поскольку алгоритм, который я использую, бесстыдно украден из умного ответа Анны Джокелы . который был бесстыдно украден из умного ответа Ильмале .
Кроме того, он даже недействителен - в нем есть неточности, вызванные числами с плавающей запятой (было бы лучше, если бы Lua поддерживал 64-битные целые числа). Тем не менее, я все еще загружаю его, просто чтобы показать, насколько быстро это решение. Это динамический язык программирования, и все же я могу вычислить n = 16 за разумное время (1 минута на 800 МГц CPU).
Запустите с LuaJIT, стандартный интерпретатор работает слишком медленно.
источник
long long
вместоdouble
настройки компиляции), а не LuaJIT.