На этом сайте было миллиард итераций задач Фибоначчи, поэтому давайте добавим, что задача Фибоначчи состоит из миллиарда итераций!
Ваша задача - вывести первые 1000 десятичных цифр из 1 000 000 000-го числа Фибоначчи с как можно более короткой программой. Это может затем произвольно сопровождаться любым дополнительным выводом по вашему выбору, включая, но не ограничиваясь, остальные цифры.
Я использую конвенции , что fib 0 = 0
, fib 1 = 1
.
Ваша программа должна быть достаточно быстрой, чтобы вы могли запустить ее и проверить ее правильность. Для этого вот первые 1000 цифр:
7952317874554683467829385196197148189255542185234398913453039937343246686182519370050999626136556779332482035723222451226291714456275648259499530612111301255499879639516053459789018700567439946844843034599802419924043753401950114830107234265037841426980398387360784284231996457340782784200767760907777703183185744656536253511502851715963351023990699232595471322670365506482435966586886048627159716916351448788527427435508113909167963907380398242848033980110276370544264285032744364781198451825462130529529633339813483105771370128111851128247136311414208318983802526907917787094802217750859685116363883374847428036737147882079956688807509158372249451437519320162582002000530798309887261257028201907509370554232931107084976854715833585623910450679449120011564762925649144509531904684984417002512086504020779012501356177874199605085558317190905395134468919443313026824813363234190494375599262553025466528838122639433600483849535070647711986769279568548796855207684897741771784375859496425384355879105799
code-golf
kolmogorov-complexity
fibonacci
restricted-time
user1502040
источник
источник
Your program must be fast enough for you to run it and verify its correctness.
как насчет памяти?a+=b;b+=a;
цикл (возможно, с Java BigInteger) является очевидным выбором, по крайней мере, если вы даже думаете о производительности. Рекурсивная реализация всегда казалась мне ужасно неэффективной.write()
системного вызова). Мне нравятся требования к производительности, это сделало меня более увлекательным.Ответы:
Python 2 + sympy, 72 байта
Попробуйте онлайн!
-10 байт, удаляя термин «практически 0» благодаря Джеффу Деге,
-1 байт (1000 -> 1e3 благодаря Захари)
-2 байт, удаляя ненужную переменную, благодаря Эрику Аутгольферу
-2 байта, переходя на Python 2 благодаря Захари
-3 байта за 11 '
-11
благодарности ThePirateBay -3 байта путем заменыstr
бэкстиков благодаря notjaganтеперь превосходит неопубликованное решение OP на Haskell!
источник
from sympy import*;sqrt
экономит байтов большеimport sympy;sympy.sqrt
:)sympy
- это символьный математический пакет для Python, поэтому с ошибкой округления проблем не возникает, по крайней мере, до очень больших чисел (это число не достаточно большое, lol). Затем я просто вычисляю его, чтобы дать мне первые 1e3 цифры, потому что в противном случае, если вы удалите.evalf(1e3)
часть, это даст мне очень короткое представление в научной нотации.Python 2 , 106 байт
Попробуйте онлайн!
Нет библиотек, только целочисленная арифметика. Запускается практически мгновенно.
Основой является тождество «разделяй и властвуй»:
Это позволяет нам обновляться
(a,b) = (f(n),f(n+1))
в два разаn -> 2*n
. Поскольку мы хотим получитьn=10**9
, это занимает толькоlog_2(10**9)=30
итерации. Мы строимn
до10**9
, неоднократно делаяn->2*n+c
для каждой цифрыc
своего двоичного расширения. Когдаc==1
удвоенное значение сдвигается вверх2*n -> 2*n+1
с одношаговым сдвигом Фибоначчи(a,b)=(b+a,b)
Чтобы сохранить
a,b
управляемые значения , мы храним только их первые1006
цифры путем деления на полы10
до тех пор, пока они не станут меньше2**3340 ~ 1e1006
.источник
a,b,c=a*a+b*b,a*a-c*c,b*b+c*c
.32-разрядный машинный код x86 (с системными вызовами Linux):
106105 байтchangelog: сохранил байт в быстрой версии, поскольку постоянная константа не меняет результат для Fib (1G).
Или 102 байта для 18% более медленной (на Skylake) версии (используя
mov
/sub
/cmc
вместоlea
/cmp
во внутреннем цикле, чтобы генерировать вынос и обертывание10**9
вместо2**32
). Или 101 байт для ~ 5,3x более медленной версии с ветвью в обработке переноса в самом внутреннем цикле. (Я измерил 25,4% ошибок в ветвях!)Или 104/101 байт, если разрешен начальный ноль. (Требуется 1 дополнительный байт для жесткого кодирования, пропуская 1 цифру вывода, что и требуется для Fib (10 ** 9)).
К сожалению, режим NASM в TIO игнорируется
-felf32
флагами компилятора. В любом случае, вот ссылка на мой полный исходный код со всеми путаницами экспериментальных идей в комментариях.Это полная программа . Он печатает первые 1000 цифр Fib (10 ** 9), за которыми следуют несколько дополнительных цифр (последние несколько из которых неправильные), за которыми следуют некоторые байты мусора (не включая символ новой строки). Большая часть мусора не ASCII, так что вы можете захотеть пройти через
cat -v
. Это не нарушает мой эмулятор терминала (KDEkonsole
). «Мусорные байты» хранят Fib (999999999). У меня уже был-1024
в реестре, так что было дешевле печатать 1024 байта, чем правильный размер.Я считаю только машинный код (размер текстового сегмента моего статического исполняемого файла), а не пух, который делает его исполняемым ELF. ( Возможны очень маленькие исполняемые файлы ELF , но я не хотел беспокоиться об этом). Оказалось, что использовать стековую память вместо BSS короче, поэтому я могу оправдать то, что в двоичном коде я ничего не считаю, поскольку не зависим от метаданных. (При обычном преобразовании статического двоичного файла получается исполняемый файл ELF размером 340 байт.)
Из этого кода вы могли бы сделать функцию, которую вы могли бы вызывать из C. Для сохранения / восстановления указателя стека (возможно, в регистре MMX) и некоторых других служебных расходов потребовалось бы несколько байтов, а также для сохранения байтов путем возврата со строкой в памяти вместо
write(1,buf,len)
системного вызова. Я думаю, что игра в гольф в машинном коде должна немного ослабить меня, поскольку никто другой даже не опубликовал ответ на каком-либо языке без встроенной расширенной точности, но я думаю, что функциональная версия этого файла все равно должна иметь размер менее 120 байт без повторной обработки всего вещь.Алгоритм:
грубая сила
a+=b; swap(a,b)
, усеченная по мере необходимости, чтобы сохранить только первые> = 1017 десятичных цифр. Он запускается за 1 мин 13 с на моем компьютере (или 322,47 млрд тактовых циклов + - 0,05%) (и может быть на несколько% быстрее с несколькими дополнительными байтами размера кода или до 62 с гораздо большим размером кода при развертывании цикла. Нет умная математика, просто делаю ту же работу с меньшими накладными расходами). Он основан на реализации Python @ AndersKaseorg , которая работает на моем компьютере за 12 минут 35 секунд (4,4 ГГц Skylake i7-6700k). Ни в одной версии нет ошибок кэша L1D, поэтому мой DDR4-2666 не имеет значения.В отличие от Python, я храню числа с расширенной точностью в формате, который освобождает усечение десятичных цифр . Я храню группы из 9 десятичных цифр на 32-разрядное целое число, поэтому смещение указателя отбрасывает младшие 9 цифр. Фактически это базовый 1 миллиард, то есть степень 10. (Это чистое совпадение, что для этого вызова требуется 1-миллиардное число Фибоначчи, но оно спасает меня пару байтов против двух отдельных констант.)
Следуя терминологии GMP , каждый 32-разрядный фрагмент номера с расширенной точностью называется «конечностью». Выполнение при добавлении должно быть сгенерировано вручную со сравнением с 1e9, но затем обычно используется в качестве входных данных для обычной
ADC
инструкции для следующего лимба. (Я также должен вручную переносить в[0..999999999]
диапазон, а не в 2 ^ 32 ~ = 4.295e9. Я делаю это безlea
+ ветвленийcmov
, используя результат выполнения сравнения.)Когда последняя ветвь производит ненулевое выполнение, следующие две итерации внешнего цикла читаются на 1 конечность выше, чем обычно, но все еще записывают в то же место. Это похоже на выполнение
memcpy(a, a+4, 114*4)
сдвига вправо на 1 конечность, но в рамках следующих двух циклов сложения. Это происходит каждые ~ 18 итераций.Хаки для экономии размера и производительности:
Обычные вещи вроде
lea ebx, [eax-4 + 1]
вместо тогоmov ebx, 1
, когда я это знаюeax=4
. И использованиеloop
в местах, гдеLOOP
медлительность оказывает лишь незначительное влияние.Обрежьте на 1 конечность бесплатно, сместив указатели, с которых мы читаем, при этом продолжая запись в начало буфера во
adc
внутреннем цикле. Мы читаем[edi+edx]
и пишем[edi]
. Таким образом, мы можем получитьedx=0
или4
получить смещение чтения-записи для места назначения. Мы должны сделать это для двух последовательных итераций, сначала смещая обе, а затем смещая только dst. Мы обнаруживаем 2-й случай, посмотревesp&4
перед сбросом указателей на переднюю часть буферов (используя&= -1024
, потому что буферы выровнены). Смотрите комментарии в коде.Среда запуска процессов в Linux (для статического исполняемого файла) обнуляет большинство регистров, а память стека ниже
esp
/rsp
обнуляется. Моя программа использует это преимущество. В этой версии с вызываемой функцией (где нераспределенный стек может быть грязным), я мог бы использовать BSS для обнуленной памяти (по стоимости, возможно, еще 4 байта для установки указателей). Обнулениеedx
заняло бы 2 байта. ABI System V x86-64 не гарантирует ни того, ни другого, но реализация Linux делает его нулевым (чтобы избежать утечки информации из ядра). В динамически связанном процессе/lib/ld.so
выполняется до_start
и оставляет регистры ненулевыми (и, вероятно, мусор в памяти ниже указателя стека).Я держу
-1024
вebx
для использования снаружи петель. Используйтеbl
в качестве счетчика для внутренних циклов, заканчивающихся нулем (который является младшим байтом-1024
, таким образом, восстанавливая константу для использования вне цикла). В Intel Haswell и более поздних версиях нет штрафов за частичное объединение регистров для регистров low8 (и фактически даже не переименовывают их отдельно) , поэтому существует зависимость от полного регистра, как у AMD (здесь нет проблем). Это было бы ужасно для Nehalem и более ранних, хотя, у которых есть срывы частичного регистра при слиянии. Есть и другие места, где я пишу частичные регистры, а затем читаю полныйxor
регистр без нуля илиmovzx
обычно потому, что я знаю, что какой-то предыдущий код обнулял верхние байты, и опять же, это нормально для AMD и семейства Intel SnB, но медленно для Intel до Sandybridge.Я использую
1024
в качестве количества байтов для записи в stdout (sub edx, ebx
), поэтому моя программа печатает некоторые байты мусора после цифр Фибоначчи, потому чтоmov edx, 1000
стоит больше байтов.(не используется)
adc ebx,ebx
с EBX = 0, чтобы получить EBX = CF, экономя 1 байт противsetc bl
.dec
/jnz
внутриadc
цикла сохраняет CF, не вызывая частичногоadc
останова флага, когда читает флаги на Intel Sandybridge и позже. Это плохо на более ранних процессорах , но AFAIK бесплатно на Skylake. Или, в худшем случае, лишний уп.Используйте память ниже
esp
как гигантскую красную зону . Поскольку это полноценная программа для Linux, я знаю, что не установил никаких обработчиков сигналов, и что ничто другое не будет асинхронно захламлять память стека пространства пользователя. Это может быть не так в других ОС.Воспользуйтесь механизмом стека, чтобы сэкономить пропускную способность проблемы uop, используя
pop eax
(1 моп + случайный стековый синхронизатор мопов) вместоlodsd
(2 моп на Haswell / Skylake, 3 на IvB и более ранние в соответствии с таблицами инструкций Agner Fog )). Во IIRC это время работы сократилось с 83 секунд до 73. Вероятно, я мог бы получить ту же скорость при использовании amov
с индексным режимом адресации, например,mov eax, [edi+ebp]
гдеebp
хранится смещение между буферами src и dst. (Это сделало бы код вне внутреннего цикла более сложным, так как пришлось бы отменять регистр смещения как часть обмена src и dst для итераций Фибоначчи.) Подробнее см. В разделе «производительность» ниже.начните последовательность, предоставив первой итерации перенос (один байт
stc
) вместо сохранения1
в памяти где-либо. Множество других проблемных вещей, задокументированных в комментариях.Список NASM (машинный код + источник) , сгенерированный с помощью
nasm -felf32 fibonacci-1G.asm -l /dev/stdout | cut -b -28,$((28+12))- | sed 's/^/ /'
. (Затем я удалил некоторые блоки комментариев, чтобы нумерация строк имела пропуски.) Чтобы удалить ведущие столбцы, чтобы вы могли передать их в YASM или NASM, используйтеcut -b 27- <fibonacci-1G.lst > fibonacci-1G.asm
.Возможно, из этого есть место для игры в гольф, но я уже потратил на это как минимум 12 часов за 2 дня. Я не хочу жертвовать скоростью, даже если она более чем достаточно быстра и есть место, чтобы уменьшить ее так, чтобы это стоило скорости . Одна из причин, по которой я пишу, - это то, как быстро я могу сделать асим-версию методом грубой силы. Если кто-то действительно хочет использовать минимальный размер, но, возможно, в 10 раз медленнее (например, 1 цифра на байт), не стесняйтесь скопировать его в качестве отправной точки.
Результирующий исполняемый файл (из
yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
) - 340B (лишенный):Представление
Внутренний
adc
цикл - 10 мопов с плавкой областью на Skylake (+1 мера синхронизации стека каждые ~ 128 байт), поэтому он может выдавать один раз за ~ 2,5 цикла на Skylake с оптимальной пропускной способностью внешнего интерфейса (игнорируя моты стековой синхронизации) , Задержка критического пути составляет 2 цикла для цепочки зависимостей, переносимой в цикле следующей итерацииadc
->cmp
->adc
, поэтому узким местом должно быть ограничение исходной задачи ~ 2,5 цикла на итерацию.adc eax, [edi + edx]
2 неопубликованных мопа для портов выполнения: нагрузка + ALU. Микроплавкие предохранители в декодерах (1 моп в слитых доменах), но на стадии выдачи не расслаиваются до 2 мопов в слитых доменах из-за режима индексированной адресации даже в Haswell / Skylake . Я думал, что он останется с микросинтеграцией, как иadd eax, [edi + edx]
делает, но, возможно, сохранение индексированных режимов адресации с микроплавлением не работает для мопов, у которых уже есть 3 входа (флаги, память и место назначения). Когда я писал это, я думал, что у него не будет снижения производительности, но я ошибался. Этот способ обработки усечения замедляет внутренний цикл каждый раз, будь тоedx
0 или 4.Было бы быстрее обрабатывать смещение чтения-записи для dst путем смещения
edi
и использованияedx
для настройки хранилища. Итакadc eax, [edi]
/ ... /mov [edi+edx], eax
/lea edi, [edi+4]
вместоstosd
. Haswell и более поздние могут хранить индексированный магазин в микроплавлении. (Sandybridge / IvB тоже не расслаивается.)На Intel Haswell и ранее,
adc
иcmovc
2 микрооперации каждая, 2с латентности . (adc eax, [edi+edx]
все еще не ламинирован на Haswell и выпускается как 3 мопа слитых доменов). Broadwell и более поздние версии допускают 3-х входные мопы для большего, чем просто FMA (Haswell), делаяadc
иcmovc
(и пару других) однопроцессных инструкций, как это было в AMD в течение долгого времени. (Это одна из причин, по которой AMD долгое время преуспевала в тестах GMP с расширенной точностью.) В любом случае, внутренний цикл Haswell должен составлять 12 мопов (иногда - +1 стек-синхронизация мопов), с внешним узким местом ~ 3c в в лучшем случае, игнорируя стековые синхронизацииИспользование
pop
без балансировкиpush
внутри цикла означает, что цикл не может выполняться из LSD (детектора потока цикла) , и его необходимо каждый раз перечитывать из кэша UOP в IDQ. Во всяком случае, это хорошая вещь на Skylake, так как цикл 9 или 10 моп не дает оптимальной выдачи при 4 моп в каждом цикле . Это, вероятно, часть того, почему заменаlodsd
наpop
так сильно помогла. (ЛСД не может заблокировать мопы, потому что это не оставит места для вставки мера стековой синхронизации .) (Кстати, обновление микрокода полностью отключает ЛСД на Skylake и Skylake-X, чтобы исправить ошибку. Я измерил выше, прежде чем получить это обновление.)Я профилировал его на Haswell и обнаружил, что он работает с 381,31 млрд тактов (независимо от частоты процессора, поскольку он использует только кэш L1D, а не память). Пропускная способность переднего плана составляла 3,72 мопов слитых доменов за такт против 3,70 для Skylake. (Но, конечно , инструкции за цикл снизился до 2,42 с 2,87, потому что
adc
иcmov
2 микрооперации на Haswell.)push
замена,stosd
вероятно, не очень поможет, потомуadc [esp + edx]
что каждый раз будет запускать синхронизацию стека. И будет стоить байт,std
так чтоlodsd
идет в другом направлении. (mov [edi], eax
/lea edi, [edi+4]
заменитьstosd
- это выигрыш: от 32 909 циклов для 100M итеров до 31 954 циклов для 100 миллионов итеров. Кажется, чтоstosd
декодируется как 3 мопа, а маны store-address / store-data не микросопряжены, поэтомуpush
+ синхронизация стека мопс все равно может быть быстрее чемstosd
)Фактическая производительность ~ 322,47 миллиардов циклов для 1G итераций 114 конечностей составляет 2,824 цикла на итерацию внутреннего цикла для быстрой версии 105B на Skylake. (Смотрите
ocperf.py
вывод ниже). Это медленнее, чем я предполагал из статического анализа, но я игнорировал издержки внешнего цикла и любые операции синхронизации стека.Perf подсчитывает
branches
иbranch-misses
показывает, что внутренний цикл неверно предсказывает один раз за внешний цикл (на последней итерации, когда она не берется). Это также составляет часть дополнительного времени.Я мог бы сохранить размер кода, установив для самого внутреннего цикла 3-тактовую задержку для критического пути, используя
mov esi,eax
/sub eax,ebp
/cmovc eax, esi
/cmc
(2 + 2 + 3 + 1 = 8B) вместоlea esi, [eax - 1000000000]
/cmp ebp,eax
/cmovc
(6 + 2 + 3 = 11B ).cmov
/stosd
Выключен критический путь. (Операция increment-ediop ofstosd
может запускаться отдельно от хранилища, поэтому каждая итерация разветвляется на короткую цепочку зависимостей.) Раньше она сохраняла еще 1B, изменяя инструкцию ebp init сlea ebp, [ecx-1]
наmov ebp,eax
, но я обнаружил, что неправильноebp
не изменил результат. Это позволило бы конечности быть точно == 1000000000 вместо переноса и создания переноса, но эта ошибка распространяется медленнее, чем рост Fib (), так что это не приводит к изменению первых 1k цифр конечного результата. Кроме того, я думаю, что ошибка может исправить себя, когда мы просто добавляем, так как в конечности есть место, чтобы держать ее без переполнения. Даже 1G + 1G не переполняет 32-разрядное целое число, поэтому оно в конечном итоге будет просачиваться вверх или обрезаться.Версия с задержкой 3c составляет 1 дополнительный такт, поэтому интерфейс может выдавать ее один раз в 2,75 циклов на Skylake, лишь немного быстрее, чем сервер может его запустить. (На Haswell это будет всего 13 мопов, так как он все еще использует
adc
иcmov
, и узкое место на входе составляет 3.25c на итера).На практике он работает в 1,18 раза медленнее на Skylake (3,34 цикла на конечность), чем на 3 / 2,5 = 1,2, который я предсказал для замены входного узкого места узким местом с задержкой от простого просмотра внутреннего цикла без синхронизации стека микрооперации. Поскольку стековые синхронизации синхронизируют только быструю версию (узкое место во внешнем интерфейсе вместо задержки), объяснение этого не займет много времени. например, 3 / 2,54 = 1,18.
Еще один фактор заключается в том, что версия с задержкой 3c может обнаруживать ошибочный прогноз при выходе из внутреннего цикла, в то время как критический путь все еще выполняется (потому что внешний интерфейс может опередить серверную часть, позволяя незапланированному выполнению запустить цикл. counter uops), поэтому эффективный штраф за неправильный прогноз ниже. Потеря этих циклов переднего плана позволяет фону наверстать упущенное.
Если бы не это, мы могли бы ускорить
cmc
версию 3c , используя ветвь во внешнем цикле вместо обработки без ветвей смещений carry_out -> edx и esp. Предсказание ветвлений + спекулятивное выполнение для управляющей зависимости вместо зависимости от данных может позволить следующей итерации запуститьadc
цикл, пока мопы из предыдущего внутреннего цикла все еще находились в полете. В версии без ответвлений адреса загрузки во внутреннем цикле зависят от данных CF последнейadc
из последней ветви.Версия с внутренним контуром с задержкой 2c является узким местом на передней части, так что внутренняя часть в значительной степени не отстает. Если бы во внешнем цикле был код с высокой задержкой, внешний интерфейс мог бы выполнить выдачу мопов из следующей итерации внутреннего цикла. (Но в этом случае во внешнем цикле много ILP и нет элементов с высокой задержкой, поэтому бэк-энду не нужно сильно догонять, когда он начинает жевать мопы в планировщике вне очереди, как их входы становятся готовыми).
( +- x %)
стандартное отклонение для 4 прогонов для этого количества. Интересно, что запускается такое круглое количество инструкций. Эти 924 миллиарда не случайность. Я предполагаю, что внешний цикл выполняет 924 инструкции.uops_issued
- это число слитых доменов (относится к полосе пропусканияuops_executed
внешней проблемы), а число неиспользуемых доменов (количество мопов, отправленных на порты выполнения). Micro-fusion упаковывает 2 мопа с неиспользованным доменом в один моп с объединенным доменом, но удаление с помощью mov означает, что некоторым мопам с плавким доменом не требуются порты выполнения См. Связанный вопрос для получения дополнительной информации о подсчете мопов и слитых и не слитых доменов. (Также смотрите таблицы инструкций Agner Fog и руководство по uarch и другие полезные ссылки в вики-тэге SO x86 ).С другой стороны, измеряя разные вещи: пропуски L1D-кэша совершенно незначительны, как и ожидалось для чтения / записи тех же двух буферов 456B. Ветвь внутренней петли неверно предсказывает один раз за внешнюю петлю (когда не требуется выходить из петли). (Общее время выше, потому что компьютер не был полностью бездействующим. Возможно, какое-то время другое логическое ядро было активным, и в прерываниях было потрачено больше времени (поскольку частота, измеренная в пространстве пользователя, была ниже 4,400 ГГц). Или несколько ядер были активны большую часть времени, снижая максимальный турбо. Я не отслеживал,
cpu_clk_unhalted.one_thread_active
было ли соревнование HT проблемой.)Мой код вполне может выполняться за меньшее количество циклов на Ryzen, который может выдавать 5 мопов за цикл (или 6, когда некоторые из них представляют собой 2-х тактные инструкции, такие как AVX 256b на Ryzen). Я не уверен, что его интерфейс будет делать с
stosd
3 мопами на Ryzen (так же, как Intel). Я думаю, что другие инструкции во внутреннем цикле имеют ту же задержку, что и Skylake, и все одиночные операции. (В том числеadc eax, [edi+edx]
, что является преимуществом перед Skylake).Вероятно, это может быть значительно меньше, но, возможно, в 9 раз медленнее, если я сохраню числа как 1 десятичную цифру на байт . Генерация выполнения с
cmp
настройкойcmov
и работа с ними будет работать одинаково, но выполнять 1/9 работы. 2 десятичных знака на байт (base-100, а не 4-битный BCD с медленнымDAA
) также будут работать, иdiv r8
/ /add ax, 0x3030
превращает 0-99 байт в две ASCII-цифры в порядке печати. Но 1 цифра на байт совсем не нужнаdiv
, просто зацикливание и добавление 0x30. Если я сохраню байты в порядке печати, это сделает 2-й цикл действительно простым.Использование 18 или 19 десятичных цифр на 64-разрядное целое число (в 64-разрядном режиме) позволило бы запустить его примерно в два раза быстрее, но стоило бы значительного размера кода для всех префиксов REX и для 64-разрядных констант. 32-битные конечности в 64-битном режиме не позволяют использовать
pop eax
вместоlodsd
. Я мог бы по-прежнему избегать использования префиксов REX, используяesp
регистр нуля без указателя (обмениваясь использованиемesi
иesp
), вместо использованияr8d
в качестве восьмого регистра.При создании версии с вызываемой функцией преобразование в 64-битную версию и ее использование
r8d
может оказаться дешевле, чем сохранение / восстановлениеrsp
. 64-битный также не может использовать однобайтовуюdec r32
кодировку (так как это префикс REX). Но в основном я использовалdec bl
2 байта. (Потому что у меня есть константа в старших байтахebx
, и я использую ее только вне внутренних циклов, что работает, потому что младший байт константы есть0x00
.)Высокопроизводительная версия
Для достижения максимальной производительности (не код-гольф) вам нужно развернуть внутренний цикл, чтобы он выполнялся не более 22 итераций, что является достаточно коротким шаблоном «взят / не принят», чтобы предсказатели ветвлений работали хорошо. В моих экспериментах
mov cl, 22
перед.inner: dec cl/jnz .inner
циклом было очень мало ошибочных прогнозов (например, 0,05%, намного меньше, чем один за полный цикл внутреннего цикла), ноmov cl,23
ошибочно прогнозировалось от 0,35 до 0,6 раза за внутренний цикл.46
особенно плохо, неправильно прогнозирует ~ 1,28 раза за внутренний цикл (128M раз за 100M итераций внешнего цикла).114
ошибочно прогнозируется ровно один раз для каждого внутреннего цикла, как я обнаружил в рамках цикла Фибоначчи.Мне стало любопытно, и я попробовал это, развернув внутренний цикл на 6 с
%rep 6
(потому что это делит 114 равномерно). Это в основном устраняет промахи веток. Я сделалedx
отрицательный результат и использовал его как офсет дляmov
магазинов, так чтоadc eax,[edi]
смог остаться в микрослиянии. (И чтобы я мог избежатьstosd
). Я вытащилlea
для обновленияedi
из%rep
блока, так что он делает только одно обновление указателя на 6 магазинов.Я также избавился от всей частичной регистрации во внешнем цикле, хотя я не думаю, что это было важно. Возможно, это немного помогло иметь CF в конце внешнего цикла, не зависящего от конечного АЦП, так что некоторые из внутренних циклов цикла могут начаться. Код внешнего цикла, вероятно, можно было бы оптимизировать немного больше, поскольку это
neg edx
было последнее, что я сделал после заменыxchg
всего лишь двумяmov
инструкциями (так как у меня уже было 1) и перестановки цепочек dep вместе с удалением 8-битного зарегистрировать вещи.Это источник NASM только петли Фибоначчи. Это вставная замена для этого раздела оригинальной версии.
Представление:
Это для того же Fibre (1G), производящего тот же результат за 62,3 секунды вместо 73 секунд. (273,146G циклов против 322,467G. Поскольку все попадает в кэш L1, тактовые частоты ядра - это действительно все, на что мы должны смотреть.)
Обратите внимание на гораздо более низкое общее
uops_issued
количество, значительно нижеuops_executed
количества. Это означает, что многие из них были микросинхронизированы: 1 моп в объединенном домене (выпуск / ROB), но 2 моп в незадействованном домене (планировщик / исполнительные блоки)). И некоторые из них были устранены на этапе выпуска / переименования (например,mov
копирование регистра илиxor
-зеро, которые должны быть выданы, но не нуждаются в исполнительном модуле). Исключенные мопы нарушат баланс в другом направлении.branch-misses
до ~ 400k, с 1G, поэтому развертывание сработало.resource_stalls.any
теперь важно, что означает, что передний конец больше не является узким местом: вместо этого задний конец отстает и ограничивает передний конец.idq_uops_not_delivered.core
учитывает только циклы, в которых интерфейс не доставлял мопы, но сервер не остановился. Это хорошо и низко, указывая на несколько узких мест переднего плана.Забавный факт: версия на python тратит больше половины своего времени на деление на 10, а не на добавление. (Замена
a/=10
сa>>=64
ускоряет его более чем в 2 раза, но меняет результат, потому что двоичное усечение! = Десятичное усечение.)Моя версия asm, конечно, оптимизирована специально для этого размера проблемы, с жестко запрограммированными счетчиками итераций цикла. Даже смещение числа произвольной точности скопирует его, но моя версия может просто прочитать смещение за следующие две итерации, чтобы пропустить даже это.
Я профилировал версию python (64-битный python2.7 на Arch Linux):
Числа в (parens) показывают, сколько времени отбор проб осуществлялся с помощью счетчика перфорирования. При просмотре большего количества счетчиков, чем поддерживает HW, перфоманс переключается между различными счетчиками и экстраполирует. Это вполне нормально для длительного выполнения одной и той же задачи.
Если бы я запускал
perf
после установки sysctlkernel.perf_event_paranoid = 0
(или запускаperf
от имени root), он бы измерял4.400GHz
.cycles:u
не учитывает время, потраченное на прерывания (или системные вызовы), только циклы пользовательского пространства. Мой рабочий стол почти полностью простаивал, но это типично.источник
Haskell,
8361 байтВыходы ( F 1000000000 , F 1000000001 ). На моем ноутбуке он правильно печатает левую пареню и первые 1000 цифр в течение 133 секунд, используя 1,35 ГБ памяти.
Как это устроено
Рецидив Фибоначчи может быть решен с использованием матричного возведения в степень:
[ F i - 1 , F i ; F i , F i + 1 ] = [0, 1; 1, 1] я ,
из которого мы получаем эти идентичности:
[ F i + j - 1 , F i + j ; F i + j , F i + j + 1 ] = [ F i - 1 , F i ; F i , F i + 1 ] ⋅ [ F j - 1 , F j ; F j , F j + 1 ],
F i + j = F i+ 1 F j + 1 - F i - 1 F j - 1 = F i + 1 F j + 1 - ( F i + 1 - F i ) ( F j + 1 - F j ),
F i + j + 1 = F i F j + F i + 1 F j + 1 .
В
p
функции вычисляет ( Р я + J , Р я + J + 1 ) с учетом ( Р я , Р я + 1 ) и ( F J , F J + 1 ). Пишемf n
для ( F i , F i + 1 ), имеемp (f i) (f j)
=f (i + j)
.Затем,
(t=<<t.p) (f i)
=
t ((t.p) (f i)) (f i)
=
t (p (f i).p (f i).p (f i)) (f i)
=
(p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i)) (f i)
=
f (10 * i)
,(t$t=<<t.p) (f i)
=
((t=<<t.p).(t=<<t.p).(t=<<t.p)) (f i)
=
f (10^3 * i)
,t(t$t=<<t.p) (f i)
=
((t$t=<<t.p).(t$t=<<t.p).(t$t=<<t.p)) (f i)
=
f (10^9 * i)
,и мы подключаем
f 1
=(1,1)
.источник
Математика, 15
34байтовFibonacci
само по себе занимает ~ 6с на моем компьютере. И 95 (+/- 5) с для внешнего интерфейса, чтобы отобразить его.Первые 1000 цифр (34 байта):
⌊Fibonacci@1*^9/1*^208986640⌋&
Дольше, но быстрее
ToString@Fibonacci@1*^9~StringTake~1000&
:источник
div
). Я остановился, так как люди, вероятно, закончили бы смотреть на этот вопрос к тому времени, когда у меня была функция игры в гольф, которая выполняла всю эту работу. Но очевидно, что грубая сила может работать, как показывают некоторые ответы.Python 2, 70 байт
На моем ноутбуке это заняло 18 минут и 31 секунду, после чего были введены правильные 1000 цифр
74100118580
(правильные следующие цифры74248787892
).источник
div
цикл, чтобы сделать 9 десятичных цифр на блок. Переносить при сложении с помощью cmp / cmov и 2xADD вместо ADC.Haskell , 78 байт
Попробуйте онлайн!
На TIO ушло 48 секунд. Та же самая рекурсивная формула, что и в моем Python-ответе , но без усечения.
Константа
2143923439
в10**9-1
обратном порядке в двоичном виде и с дополнительным 1 в конце. Итерация по двоичным цифрам в обратном порядке имитирует итерацию по двоичным цифрам10**9-1
. Кажется, короче жестко закодировать это, чем вычислить.источник
Haskell ,
202184174173170168164162 байтаПопробуйте онлайн!
объяснение
Это использует довольно быстрый способ для вычисления чисел Фибоначчи. Функция
l
берет два числа Фибоначчи и вычисляет числа Фибоначчи 10 позже, аf
берет n- е и n + 1- е числа Фибоначчи и вычисляет 2n + 20- е и 2n + 21- е числа Фибоначчи. Я цепляю их довольно случайно, чтобы получить 1 миллиард и получить первые 1000 цифр.источник
Haskell, 81 байт
объяснение
f n
Рекурсивно вычисляет числоn
Фибоначчи, используя рекурсию из ответа xnor с устранением общего подвыражения. В отличие от других опубликованных решений, которые используют умножения O (log (n)), мы имеем рекурсию глубины O (log (n)) - с коэффициентом ветвления 2 для сложности умножений O (n).Однако еще не все потеряно! Поскольку почти все вызовы будут находиться в нижней части дерева рекурсии, мы можем по возможности использовать быструю встроенную арифметику и избегать множества манипуляций с огромными бигнумами. Через пару минут он выкладывает ответ на мой ящик.
источник
T-SQL,
422 414453 байта (проверено, теперь конкурирует!)РЕДАКТИРОВАТЬ 2 : Изменено , получил несколько байтов, но увеличил скорость достаточно, чтобы завершить до 1 миллиарда! Завершено за 45 часов 29 минут , проверяет соответствие данной строке и отображает дополнительные 8 символов (что может быть или не быть правильным из-за ошибок округления).
INT BIGINT
DECIMAL(37,0)
T-SQL не имеет встроенной поддержки «огромного числа», поэтому пришлось свернуть мой собственный текстовый сумматор огромных чисел, используя строки из 1008 символов:
Вот отформатированная версия с комментариями:
По сути, я вручную манипулирую 1008-символьными строками, заполненными нулями, представляющими две мои переменные Фибоначчи,
@a
и@
.Я добавляю их по
8 1836 цифр за раз, убирая последние 36 цифр, преобразовывая их в управляемый числовой тип (DECIMAL(37,0)
), добавляя их, а затем разбивая обратно в другую длинную строку@c
. Я тогда «поворот»@a
и@
перемещая последние 36 цифр к фронту, и повторяя процесс. 28 вращений * 36 цифр охватывает все 1008. Я должен «нести один» вручную.Как только наше число начинает превышать мою длину строки, я «сдвигаюсь влево», и мы начинаем терять некоторую точность, но ошибка вполне в моих дополнительных символах.
Я попытался использовать таблицу SQL, полную INT и BIGINT, с похожей логикой, и она была значительно медленнее. Weird.
источник
PARI / GP, 45 байт
Как-то
\p1000
не достаточно. Это не работает с 32-битными системами. Последнее разделение состоит в том, чтобы избежать десятичной точки в научной нотации.источник
Пари / ГП , 15 + 5 = 20 байт
Запустите с параметром командной строки,
-s1g
чтобы выделить 1 Гбайт памяти.источник
Рубин, 63 байта
чувак, я плохо играю в гольф-рубин; но класс BigInt делает чудеса для такого рода вещей. Мы используем тот же алгоритм, что и Anders Kaseorg.
источник