Почему этот простой тест такой медленный?

Этот код был взят из книги "Haskell Road to Logic, Math and Programming". Он реализует алгоритм решета эратосфена и решает проблему 10 проекта Эйлера.

sieve :: [Integer] -> [Integer]
sieve (0 : xs) = sieve xs
sieve (n : xs) = n : sieve (mark xs 1 n)
  where
    mark :: [Integer] -> Integer -> Integer -> [Integer]
    mark (y:ys) k m | k == m = 0 : (mark ys 1 m)
                    | otherwise = y : (mark ys (k+1) m)

primes :: [Integer]
primes = sieve [2..]

-- Project Euler #10
main = print $ sum $ takeWhile (< 2000000) primes

На самом деле он работает даже медленнее, чем наивный простой тест. Может кто-нибудь объяснить такое поведение?

Я подозреваю, что это как-то связано с повторением каждого элемента в списке в функции отметки.

Спасибо.


person anatoly    schedule 30.07.2012    source источник
comment
Отчасти актуально (но не ответ) - Подлинный сейв Эратосфена - уже есть прямая ссылка в комментарии к верхнему ответу, но это через страницу Lambda The Ultimate.   -  person Steve314    schedule 10.08.2012


Ответы (4)


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

Давайте рассмотрим, как это работает, и мы надеемся, что проблема станет очевидной. Для упрощения предположим, что мы хотим print элементы primes до бесконечности, т.е. мы хотим оценивать каждую ячейку в списке одну за другой. primes определяется как:

primes :: [Integer]
primes = sieve [2..]

Поскольку 2 не 0, применяется второе определение sieve, и 2 добавляется к списку простых чисел, а остальная часть списка представляет собой неоцененный преобразователь (я использую tail вместо совпадения с шаблоном n : xs в sieve для xs, поэтому tail на самом деле не вызывается и не добавляет никаких накладных расходов в приведенный ниже код; mark на самом деле является единственной функцией thunk):

primes = 2 : sieve (mark (tail [2..]) 1 2)

Теперь нам нужен второй элемент primes. Итак, проходим код (упражнение для читателя) и получаем:

primes = 2 : 3 : sieve (mark (tail (mark (tail [2..]) 1 2)) 1 3)

Снова та же процедура, мы хотим оценить следующее простое число ...

primes = 2 : 3 : 5 : sieve (mark (tail (tail (mark (tail (mark (tail [2..]) 1 2)) 1 3))) 1 5)

Это начинает выглядеть как LISP, но я отвлекся ... Начинаете видеть проблему? Для каждого элемента в списке primes необходимо оценивать все больше и больше стеков mark приложений. Другими словами, для каждого элемента в списке должна быть проверка того, отмечен ли этот элемент каким-либо из предыдущих простых чисел, путем оценки каждого mark приложения в стеке. Итак, для n~=2000000 среда выполнения Haskell должна вызывать функции, в результате чего получается стек вызовов с глубиной примерно ... Я не знаю, 137900 (let n = 2e6 in n / log n дает нижнюю границу)? Что-то такое. Вероятно, это причина замедления; возможно, vacuum расскажет вам больше (у меня сейчас нет компьютера с Haskell и графическим интерфейсом).

Причина, по которой решето Эратосфена работает на таких языках, как C, заключается в том, что:

  1. Вы не используете бесконечный список.
  2. Из-за (1) вы можете пометить весь массив перед переходом к следующему n, что приведет к отсутствию накладных расходов стека вызовов вообще.
person dflemstr    schedule 30.07.2012
comment
(Чтобы узнать об альтернативных алгоритмах, прочтите этот документ.) - person Louis Wasserman; 30.07.2012
comment
... и этот - person Simon Bergot; 30.07.2012
comment
Причина, по которой решето Эратосфена работает на таких языках, как C - fwiw вы также можете выполнять строгое программирование массивов в Haskell, поэтому я полагаю, что в этом отношении Haskell - это язык, подобный C. - person Dan Burton; 31.07.2012
comment
A = (1) и (2) применяются к таким языкам, как C, B = (1) и (2) применяются к таким языкам, как Haskell, P = Sieve of Eratosthenes работает. А → П; B → P. Означает ли это, что A = B? - person dflemstr; 31.07.2012
comment
вы, кажется, предполагаете, что там растет все больше и больше вложенных mark приложений, но каждое mark работает отдельно, по отдельности, фактически создавая поток чисел, каждое из которых получает свои данные от своего предшественника, именно потому, что каждое mark приложение является получение принудительного сопоставления с образцом - создание фактического списка в памяти. В Haskell хранилище является постоянным. Так что там просто нет такого растущего бубна, как вы описываете. - person Will Ness; 02.08.2012
comment
Ну, под большим преобразователем я подразумеваю преобразователь, который зависит от множества других преобразователей. Как в foldl (+) 0 xs создаст большой переходник. Это такая же зависимость от преобразователя, хотя последствия для памяти разные, как вы говорите. - person dflemstr; 02.08.2012
comment
Совершенно моя точка зрения. Такой преобразователь, созданный foldl, нигде не встречается в коде OP. (k+1) немедленно принудительно запускается следующим вызовом сравнения k==m. Других панков там не вижу. Каждый маркировочный стержень неглубокий. - person Will Ness; 03.08.2012
comment
Совершенно моя точка зрения. Представление в памяти другое, но это та же зависимость преобразователя, что и в ситуации foldl. То есть стек mark не запускается для постепенного разрешения единственного значения; вместо этого первая метка должна создать элемент, который заставляет следующую метку проходить его ввод для создания элемента и т. д. Таким образом, вытеснение элемента из общего стека подразумевает работу всех mark в стеке, вместо того, чтобы иметь возможность напр. имеют условие перехода на три уровня в глубину для перекрывающихся псевдопростых чисел (поэтому mark x 3 экранирует до срабатывания mark x 7 при проверке 21). - person dflemstr; 03.08.2012
comment
нет, это не так. внутренние переходники в складной ситуации остаются незакрепленными. Эта ситуация обычно называется неоцененным накоплением переходов, как вы это сделали здесь. но здесь нет неоцененных преобразователей. каждый вынужден своим потребителем, получателем его выхода. здесь они не вложены, а связаны цепочкой через потоки данных - вывод одного является входным для другого. каждая принудительная, мелкая. Никаких вложенных переходов. здесь детали реализации использования лямбда-выражений без аргументов, также называемых thunks, не имеют значения. они также могут быть реализованы как генераторы с изменяемым состоянием. - person Will Ness; 03.08.2012
comment
и обход не выполняется. каждый выходной элемент немедленно потребляется. Каждый производитель марки работает отдельно. В ситуации отложенных фильтров экранирования тоже нет, так что это не главная проблема. Главная проблема в том, что их слишком много. (ну, с фильтрами есть просеивание, некоторые числа забраковываются раньше - здесь нет просеивания - я также упоминаю об этом в своем ответе - но это второстепенная проблема). - person Will Ness; 03.08.2012
comment
(хорошо, убегая, вы, вероятно, имели в виду то, что я имею в виду под просеиванием ...) Возможно, я неправильно истолковал вопрос, например, почему это так медленно? В узком смысле он действительно спрашивает, почему код OP медленнее, чем у Тернерса. Это действительно из-за отсутствия просеивания (но также потому, что каждый mark производитель должен постоянно обновлять свое внутреннее состояние - увеличивать свой счетчик - в отличие от nomult в сите Тернера). ИМО до сих пор нет неоцененных преобразователей - именно потому, что каждый tail принудительно путем сопоставления с образцом. Вы упоминаете об этом в самом ответе. - person Will Ness; 03.08.2012
comment
Я думаю, что вы чрезмерно анализируете как проблему, так и то, как я решил ее описать. Я смотрю на vacuum анализ значения primes и лично считаю, что мое описание соответствует тому, что я вижу. Если есть какая-то деталь, которая заставляет мое описание иметь для вас совершенно иное значение, извините, но это также не так уж и важно; В конце концов, это ТАК вопрос, поэтому всегда есть другие ответы, предлагающие разные точки зрения; это не конец света, если не покрыть 100% ответа :) - person dflemstr; 03.08.2012

Не только преобразователи делают его ужасающе медленным, этот алгоритм также был бы очень медленным, если бы он был реализован на C на конечном массиве битов.

sieve :: [Integer] -> [Integer]
sieve (0 : xs) = sieve xs
sieve (n : xs) = n : sieve (mark xs 1 n)
  where
    mark :: [Integer] -> Integer -> Integer -> [Integer]
    mark (y:ys) k m | k == m = 0 : (mark ys 1 m)
                    | otherwise = y : (mark ys (k+1) m)

Для каждого простого p этот алгоритм проверяет все числа от p+1 до предела, являются ли они кратными p. Это делается не путем деления, как сито Тернера, а путем сравнения счетчика с простым. Теперь сравнение двух чисел происходит намного быстрее, чем деление, но за это приходится платить тем, что каждое число n проверяется на каждое простое число < n, а не только на простые числа вплоть до наименьшего простого множителя n.

В результате сложность этого алгоритма составляет O (N ^ 2 / log N) по сравнению с O ((N / log N) ^ 2) для решета Тернера (и O (N * log (log N)) для реального сито Эратосфена).

вложение ¹ штабелирование преобразователей , упомянутое dflemstr, усугубляет проблему², но даже без этого алгоритм будет хуже, чем Тернер. Я потрясен и очарован одновременно.


¹ «Вложенность» может быть неправильным словом. Хотя каждый из mark преобразователей доступен только через один над ним, они не ссылаются ни на что из области действия охватывающего преобразователя.

² Тем не менее, нет ничего квадратичного ни по размеру, ни по глубине преобразователей, и они довольно хорошо себя ведут. Для иллюстрации представим, что mark определены с обратным порядком аргументов. Тогда, когда 7 оказывается простым, ситуация

sieve (mark 5 2 (mark 3 1 (mark 2 1 [7 .. ])))
~> sieve (mark 5 2 (mark 3 1 (7 : mark 2 2 [8 .. ])))
~> sieve (mark 5 2 (7 : mark 3 2 (mark 2 2 [8 .. ])))
~> sieve (7 : mark 5 3 (mark 3 2 (mark 2 2 [8 .. ])))
~> 7 : sieve (mark 7 1 (mark 5 3 (mark 3 2 (mark 2 2 [8 .. ]))))

и следующее сопоставление с шаблоном, выполненное sieve, вызывает mark 7 1 преобразователь, который вызывает mark 5 3 преобразователь, который вызывает mark 3 2 преобразователь, который вызывает mark 2 2 преобразователь, который принудительно [8 .. ] преобразователь и заменяет головку на 0, и обертывает хвост в mark 2 1 thunk. Он поднимается до sieve, который отбрасывает 0 и затем форсирует следующую стопку преобразователей.

Таким образом, для каждого числа от p_k + 1 до p_(k+1) (включительно) сопоставление с образцом в sieve заставляет стек / цепочку k преобразователей в форме mark p r. Каждый из них принимает (y:ys), полученный от вложенного преобразователя ([y ..] для самого внутреннего mark 2 r), и оборачивает хвост ys в новый преобразователь, либо оставляя y без изменений, или заменяя его на 0, таким образом создавая новый стек / цепочку преобразователей в том, что будет хвостом списка, доходящего до sieve.

Для каждого найденного простого числа sieve добавляет еще один mark p r преобразователь сверху, поэтому в конце, когда будет найдено первое простое число больше 2000000 и завершится takeWhile (< 2000000), будет 148933 уровней преобразователей.

Укладка панелей здесь не влияет на сложность, а влияет на постоянные факторы. В ситуации, с которой мы имеем дело, когда мы имеем дело с лениво генерируемым бесконечным неизменяемым списком, мало что можно сделать, чтобы сократить время, затрачиваемое на передачу управления от одного преобразователя к другому. Если бы мы имели дело с конечным изменяемым списком или массивом, который не генерируется лениво, как в таких языках, как C или Java, было бы намного лучше позволить каждому mark p завершить свою работу полностью (это был бы простой цикл for с меньше накладных расходов, чем вызов функции / передача управления) перед исследованием следующего числа, поэтому никогда не будет более одного активного маркера и меньше передачи управления.

person Daniel Fischer    schedule 30.07.2012
comment
потрясен? быть действительно потрясенным: [n | n<-[2..], not $ elem n [j*k | j<-[1..n-1], k<-[1..n-1]]]. Это на самом деле было предложено здесь, на SO, хотя и на Python. - person Will Ness; 31.07.2012
comment
Уилл, меня не пугает плохой алгоритм как таковой. Ужасно то, что код взят из книги - предположительно хорошей - и, по-видимому, приходит без предупреждения. - person Daniel Fischer; 31.07.2012
comment
потрясен и очарован - я должен был процитировать вас более полно. :) Этот алгоритм, который я процитировал, также очаровывает меня, так как он также является истинной транскрипцией определения, как и решето Тернера. В конце концов, простые числа действительно являются целыми числами больше 1, которые не являются составными. В конце концов, Haskell - это не математика. - person Will Ness; 31.07.2012
comment
Да, этот алгоритм потрясающий. У него есть небольшой недостаток, заключающийся в том, что списки должны начинаться с 2, чтобы действительно отразить определение простого числа, но, помимо этого сбоя, это похоже на решето Тернера, аккуратное и краткое объявление, скрывающее ужасную сложность - только в большей степени. - person Daniel Fischer; 31.07.2012
comment
даже с 1s он почему-то производит простые числа. :) но это действительно что-то особенное, не так ли. Вещь, на которую стоит взглянуть. - person Will Ness; 31.07.2012
comment
Поскольку верхняя граница равна n-1, ни один продукт 1*k не может быть n, поэтому он работает. Но для n > 2 в списке j*k есть простые числа, хотя концептуально он должен содержать только составные части. Я согласен насчет специальности, это первый кубический генератор простых чисел, который я видел, потрясающе. - person Daniel Fischer; 31.07.2012
comment
Не знаю, сильно ли изменит то, что если позволить каждому mark работать до конца в свою очередь. Общее количество операций по-прежнему будет O(n), как сейчас, потому что mark считается по единицам, а не O(n/p). Независимо от того, работаете ли она по строкам или по столбцам, ячейка за ячейкой просматривает одну и ту же таблицу. - person Will Ness; 04.08.2012
comment
@Will Если у вас есть строго изменяемые данные, позволяя каждому mark выполнять свою работу за один раз, исключается создание thunk { when demanded, do that } и передача управления, каждый будет просто циклом с небольшими накладными расходами. Сложность останется прежней, но for(i = p+1, m = 1; i < N; ++i) { if (p == m) { .. }} работы намного меньше, чем вызов функций и создание преобразователей. - person Daniel Fischer; 04.08.2012
comment
это зависит от того, насколько хорошо компилятор оптимизирует обработку преобразователей. если он каждый раз вслепую создает новые полномасштабные закрытия, чтобы потом от них отказаться, тогда то, что вы говорите, имеет большой смысл. - person Will Ness; 04.08.2012
comment
Я не понимаю, как он мог сделать что-то меньшее, чем написать структуру, содержащую два числа (простое и остаток) и индекс рядом с процессом (указатель на следующий элемент в случае связанного списка) плюс указание того, какой преобразователь оценить раньше, если таковые имеются. Эти структуры можно использовать повторно, обновляя соответствующие элементы, но это все равно больше, чем просто зацикливание. Не драматично, я думаю, но знаменательно. - person Daniel Fischer; 04.08.2012
comment
истинный. хотя такие овеществленные замыкания действительно превратятся в своего рода генераторы, поэтому их нужно будет вытаскивать, а не оценивать. затем цепочка таких структур может быть преобразована в массив и обработана в цикле. затем этот цикл можно было бы развернуть, структуры можно было бы скомпилировать в набор глобальных переменных и т. д .; Оптимизаторы вмешались бы, сворачивая цепочки присваиваний, изменяя только одну переменную, ... кто знает, чем это закончится. :) - person Will Ness; 05.08.2012

Хорошо, вы точно правы, он медленнее, чем наивная реализация. Я взял это из Википедии и сравнил его с вашим кодом с GHCI следующим образом:

-- from Wikipedia
sieveW [] = [] 
sieveW (x:xs) = x : sieveW remaining 
  where 
    remaining = [y | y <- xs, y `mod` x /= 0]

-- your code
sieve :: [Integer] -> [Integer]
sieve (0 : xs) = sieve xs
sieve (n : xs) = n : sieve (mark xs 1 n)
  where
    mark :: [Integer] -> Integer -> Integer -> [Integer]
    mark (y:ys) k m | k == m = 0 : (mark ys 1 m)
                    | otherwise = y : (mark ys (k+1) m)

Бег дает

[1 of 1] Compiling Main             ( prime.hs, interpreted )
Ok, modules loaded: Main.
*Main> :set +s
*Main> sum $ take 2000 (sieveW [2..])
16274627
(1.54 secs, 351594604 bytes)
*Main> sum $ take 2000 (sieve [2..])
16274627
(12.33 secs, 2903337856 bytes)

Чтобы попытаться понять, что происходит и как именно работает код mark, я попытался расширить код вручную:

  sieve [2..]
= sieve 2 : [3..]
= 2 : sieve (mark [3..] 1 2)
= 2 : sieve (3 : (mark [4..] 2 2))
= 2 : 3 : sieve (mark (mark [4..] 2 2) 1 3)
= 2 : 3 : sieve (mark (0 : (mark [5..] 1 2)) 1 3)
= 2 : 3 : sieve (0 : (mark (mark [5..] 1 2) 1 3))
= 2 : 3 : sieve (mark (mark [5..] 1 2) 1 3)
= 2 : 3 : sieve (mark (5 : (mark [6..] 2 2)) 1 3)
= 2 : 3 : sieve (5 : mark (mark [6..] 2 2) 2 3)
= 2 : 3 : 5 : sieve (mark (mark (mark [6..] 2 2) 2 3) 1 5)
= 2 : 3 : 5 : sieve (mark (mark (0 : (mark [7..] 1 2)) 2 3) 1 5)
= 2 : 3 : 5 : sieve (mark (0 : (mark (mark [7..] 1 2) 3 3)) 1 5)
= 2 : 3 : 5 : sieve (0 : (mark (mark (mark [7..] 1 2) 3 3)) 2 5))
= 2 : 3 : 5 : sieve (mark (mark (mark [7..] 1 2) 3 3)) 2 5)
= 2 : 3 : 5 : sieve (mark (mark (7 : (mark [8..] 2 2)) 3 3)) 2 5)

Я думаю, что, возможно, я допустил небольшую ошибку в конце, так как 7 выглядит так, как будто его вот-вот превратят в 0 и удалите, но механизм ясен. Этот код просто создает набор счетчиков, считающих каждое простое число, выдает следующее простое число в нужный момент и передает его вверх по списку. Это эквивалентно простой проверке деления на каждое предыдущее простое число, как в наивной реализации, с дополнительными накладными расходами на передачу нулей или простых чисел между преобразователями.

Здесь может быть еще одна тонкость, которую мне не хватает. Сито Эратосфена в Haskell очень подробно рассматривается вместе с различными оптимизациями, которые здесь.

person Vic Smith    schedule 30.07.2012
comment
Я не вижу, чтобы какие-либо mod вычисления в sieveW запоминались, отчасти потому, что каждый y `mod` x запрашивается только один раз (элементы xs уникальны), а отчасти потому, что даже если xs содержит дубликаты, ранее вычисленное y `mod` x было бы недоступно . - person dave4420; 30.07.2012
comment
Да, вы правы - надо было это до конца продумать. Отредактирую ответ. - person Vic Smith; 30.07.2012

краткий ответ: счетное сито работает медленнее, чем сито Тернера (также известное как «наивное»), потому что оно имитирует прямой доступ к ОЗУ с последовательным счетом, что заставляет его проходить по потокам не просмотрено между этапами разметки. Это иронично, потому что счет делает его "подлинным" ситом Эратосфена, в отличие от сита пробного деления Тернера. На самом деле удаление кратных, как это делает сито Тернера, может испортить счет.

Оба алгоритма работают очень медленно, потому что они запускают работу по удалению кратных чисел слишком рано с каждого найденного простого числа вместо его квадрата, создавая, таким образом, слишком много ненужных этапов обработки потока (будь то фильтрация или маркировка) - вместо этого O(n) из них всего ~ 2*sqrt n/log n при получении простых чисел до n. Проверка на кратность 7 не требуется, пока во входных данных не появится 49.

Этот ответ объясняет, как sieve можно рассматривать как построение конвейера потоковых "преобразователей" позади себя, поскольку он работает :

[2..] ==> sieve --> 2
[3..] ==> mark 1 2 ==> sieve --> 3
[4..] ==> mark 2 2 ==> mark 1 3 ==> sieve 
[5..] ==> mark 1 2 ==> mark 2 3 ==> sieve --> 5
[6..] ==> mark 2 2 ==> mark 3 3 ==> mark 1 5 ==> sieve 
[7..] ==> mark 1 2 ==> mark 1 3 ==> mark 2 5 ==> sieve --> 7
[8..] ==> mark 2 2 ==> mark 2 3 ==> mark 3 5 ==> mark 1 7 ==> sieve
[9..] ==> mark 1 2 ==> mark 3 3 ==> mark 4 5 ==> mark 2 7 ==> sieve
[10..]==> mark 2 2 ==> mark 1 3 ==> mark 5 5 ==> mark 3 7 ==> sieve
[11..]==> mark 1 2 ==> mark 2 3 ==> mark 1 5 ==> mark 4 7 ==> sieve --> 11

Решето Тернера использует nomult p = filter ((/=0).(`rem`p)) вместо mark _ p записей, но в остальном выглядит так же:

[2..] ==> sieveT --> 2
[3..] ==> nomult 2 ==> sieveT --> 3
[4..] ==> nomult 2 ==> nomult 3 ==> sieveT 
[5..] ==> nomult 2 ==> nomult 3 ==> sieveT --> 5
[6..] ==> nomult 2 ==> nomult 3 ==> nomult 5 ==> sieveT 
[7..] ==> nomult 2 ==> nomult 3 ==> nomult 5 ==> sieveT --> 7
[8..] ==> nomult 2 ==> nomult 3 ==> nomult 5 ==> nomult 7 ==> sieveT 

Каждый такой преобразователь может быть реализован как закрывающий фрейм (он же «преобразователь») или как генератор с изменяемым состоянием, что не имеет значения. Продукция каждого такого производителя поступает непосредственно в качестве входных данных для его преемника в цепочке. Здесь нет неоцененных преобразователей, каждый вынужден своим потребителем производить следующий результат.

Итак, чтобы ответить на ваш вопрос,

Я подозреваю, что это как-то связано с повторением каждого элемента в списке в функции отметки.

да, точно. В противном случае они оба используют схемы без отсрочки.


Таким образом, код можно улучшить, отложив начало маркировки потока:

primes = 2:3:filter (>0) (sieve [5,7..] (tail primes) 9)

sieve (x:xs) ps@ ~(p:t) q 
   | x < q = x:sieve xs ps q
   | x==q  = sieve (mark xs 1 p) t (head t^2)
  where
    mark (y:ys) k p 
       | k == p    = 0 : (mark ys 1 p)      -- mark each p-th number in supply
       | otherwise = y : (mark ys (k+1) p)

Теперь он работает на уровне чуть выше O(k^1.5), эмпирически, в k произведенных простых числах. Но зачем считать по единицам, если мы можем считать по инкрементам. (Каждое третье нечетное число из 9 можно найти, снова и снова добавляя 6.) И затем вместо того, чтобы отмечать, мы можем сразу отсеять числа, получив добросовестное сито Эратосфена (пусть и не самое эффективное):

primes = 2:3:sieve [5,7..] (tail primes) 9

sieve (x:xs) ps@ ~(p:t) q 
   | x < q = x:sieve xs ps q
   | x==q  = sieve (weedOut xs (q+2*p) (2*p)) t (head t^2)
  where
    weedOut i@(y:ys) m s 
       | y < m = y:weedOut ys m s
       | y==m  = weedOut ys (m+s) s
       | y > m = weedOut i (m+s) s

Это выполняется при более чем O(k^1.2) в k простых числах, быстрое и грязное тестирование, скомпилированное и загруженное в GHCi, дает до 100-150 тысяч простых чисел, ухудшается до O(k^1.3) при примерно 0,5 миллиона простых чисел.


Так какого же ускорения это достигается? Сравнивая код OP с ситом Тернера из "Википедии",

primes = sieve [2..] :: [Int]
  where
    sieve (x:xs) = x : sieve [y | y <- xs, rem y x /= 0]

было 8x ускорение W / OP до 2k (т. е. производилось 2000 простых чисел). Но при 4k это было 15x ускорение. Сито Тернера, кажется, работает с примерно O(k^1.9 .. 2.3) эмпирической сложностью при получении k = 1000 .. 6000 простых чисел, а счетное сито - с O(k^2.3 .. 2.6) для того же диапазона.

Для двух версий, представленных здесь в этом ответе, v1 / W был дополнительно 20x быстрее на 4k, а 43x на 8k . v2 / v1 был 5.2x на 20k, 5.8x при 40k и 6.5x быстрее при производя 80 000 простых чисел.

(для сравнения, код очереди приоритетов Мелиссы О'Нил работает с эмпирической сложностью примерно O(k^1.2), получено k простых чисел. Конечно, он масштабируется намного лучше, чем приведенный здесь код).


Вот решето из определения Эратосфена:

P = {3, 5, ...} \ {{p * p , p * p + 2 * p, ...} | p в P}

Ключ к эффективности Решета Эратосфена - это прямое генерирование кратных простых чисел путем подсчета с приращением (двойного) значения простого числа от каждого простого числа; и их прямое исключение, что стало возможным благодаря объединению значения и адреса, как и в алгоритмах целочисленной сортировки (возможно только с изменяемыми массивами). Неважно, должен ли он производить заданное количество простых чисел или работать бесконечно, потому что он всегда может работать по сегментам.

person Will Ness    schedule 31.07.2012
comment
Решето Тернера использует nomult p = filter ((/=0).(`rem`p)) .... Проблемы с разметкой в ​​теле ответа. - person Will Ness; 03.08.2012
comment
Исправлено, что для вас двойная обратная кавычка code-with-backticks двойная обратная кавычка также работает в телах вопросов. - person Daniel Fischer; 04.08.2012
comment
@DanielFischer, спасибо, я пробовал использовать двойные обратные кавычки вокруг rem внутри, а не вокруг всего этого ... Если мы уже здесь, как насчет этого мифического неоцененного накопления переходов of ... в ответе dflemstr - см. также в комментариях). Я говорю, что его не существует здесь. Цепочка зависимых производителей, каждый вынужденная своим потребителем, не является вложенным преобразователем, как это сделал бы foldl. Разве мы не должны использовать язык однозначно? Вы тоже ссылаетесь на это в своем ответе. Я считаю это ошибочным. - person Will Ness; 04.08.2012
comment
Что ж, штабелированные бочки могут быть лучшей формулой. Но у вас есть после k-го первичного преобразователя (порядок аргументов переключения) mark p_k x (mark ... (mark 3 x (mark 2 x list))...), вложенные или уложенные k слоями в глубину. Таким образом, каждое совпадение с шаблоном в sieve говорит самому внешнему преобразователю. Эй, дайте мне конструктор, который передает сообщение вниз, затем (_:_) снова передается вверх, пара преобразователей заменяет заголовок на 0 в пути (обычно). У вас есть растущее гнездо / стопка преобразователей (хотя и не квадратичная ни по размеру, ни по глубине - пропорциональная количеству найденных простых чисел), что замедляет работу. - person Daniel Fischer; 04.08.2012
comment
связаны, а не вложены. Каждый из них существует в своей памяти. Он принудительно передается через хранилище, а не изнутри функции над ним. Он не знает о функции / преобразователе / ​​производителе / ​​генераторе / преобразователе над ним, все, что он знает, - это как произвести свой следующий вывод. Таким же образом, все, что у него есть, это его ввод (y: ys) - хранилище - он не знает, откуда он, от другой отметки в цепочке или что-то еще. Во время выполнения нет mark (mark (mark ... ))). Каждую функцию создания списков mark лучше всего рассматривать как генератор, ИМО. - person Will Ness; 04.08.2012
comment
Как часто бывает, я не понимаю, что вы пытаетесь сказать. mark list преобразователь при оценке дает результат в форме (:) x' (mark xs), если list имеет форму (x:xs), где x' равно x или 0. Он оценивается только тогда, когда включающему преобразователю нужно знать, равен ли его аргумент y:ys или нет. Это происходит только тогда, когда этот преобразователь оценивается, его оценка затем дает (:) x'' (mark (mark xs)) [или, если вы предпочитаете (:) x'' thunk where thunk = mark thunk' where thunk' = mark xs]. Каждый из этих преобразователей доступен только через преобразователь над ним, мне кажется, что они вложены друг в друга. - person Daniel Fischer; 04.08.2012
comment
Если нет технического определения вложенных переходов, которое означает что-то еще, если так, я был бы признателен за ссылку. И да, во время выполнения есть mark (mark (mark ...)). Даже если вы думаете о них как о генераторах, каждый из них принимает выходные данные нижеприведенного в качестве входных данных, поэтому имеет ссылку на это. - person Daniel Fischer; 04.08.2012
comment
вложенный подразумевает вложенную область видимости. f x = g where g = z x where z = p q where p = .... определяет вложенные преобразователи. каждый относится к окружающей среде. так я думал. - person Will Ness; 04.08.2012
comment
С таким определением, конечно, они действительно не вложены. Ладно, изменю формулировку во избежание недоразумений. - person Daniel Fischer; 04.08.2012