Что-то не так с моей перетасовкой Фишера-Йейтса?

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

Я относительно новичок в Haskell, и в своих экспериментах я закодировал базовую перетасовку Фишера-Йейтса, как показано ниже.

shuffle :: RandomGen g => [a] -> g -> ([a],g)
shuffle [] g0 = ([],g0)
shuffle [x] g0 = ([x],g0)
shuffle xs g0 = (x:newtail,g2)
  where (i,g1) = randomR (0, length $ tail xs) g0
        (xs1,x:xs2) = splitAt i xs
        (newtail,g2) = shuffle (xs1++xs2) g1

Эта реализация, конечно, использует память beaucoup для больших списков, но она быстрая - на моем ноутбуке в среднем 5 с для 30 миллионов целых чисел по сравнению со стандартным перетасовкой С++ со скоростью 2,3 с). Фактически, это намного быстрее, чем другие реализации Haskell, найденные в других местах (например, http://www.haskell.org/haskellwiki/Random_shuffle)

Учитывая, что другие перетасовки Haskell, которые я видел, и сложнее, и медленнее, мне интересно, является ли ускорение/простота просто моей наградой за то, что я безжалостно пожираю память, или я упустил какую-то крошечную, но важную деталь, которая делает мой алгоритм предвзятым. Я не проверял всесторонне, но предварительный взгляд, кажется, показывает равномерное распределение перестановок.

Я был бы признателен за оценку большего количества глаз с большим опытом работы с Haskell и / или перетасовкой. Заранее большое спасибо всем, кто найдет время ответить.


person Tientuinë    schedule 26.04.2013    source источник
comment
Учитывая, что это вычисляет длину списка на каждой итерации, мне трудно поверить в указанные вами цифры производительности. Это алгоритм O(n^2).   -  person Carl    schedule 26.04.2013
comment
Возможно, вы синхронизируете алгоритм, фактически не заставляя оценивать весь результат?   -  person C. A. McCann    schedule 26.04.2013
comment
Покажите временной код. Полная перетасовка 30 миллионов Ints с этим кодом занимает несколько дней.   -  person Daniel Fischer    schedule 26.04.2013
comment
Я почти хотел бы вернуться в прошлое и перестать публиковать этот вопрос. Как неловко. :o Спасибо всем за то, что не просто высмеяли меня отсюда!   -  person Tientuinë    schedule 27.04.2013
comment
Я почти хотел бы вернуться в прошлое и перестать публиковать этот вопрос. Почему? Вы чему-то научились из этого, не так ли? Это хорошая вещь. (Вы также получили от этого некоторую репутацию, что тоже неплохо.) Итак, вы попали в ловушку недостаточного учета лени, как неловко - как будто этого не случалось с большинством из нас. Ты там в хорошей компании.   -  person Daniel Fischer    schedule 27.04.2013
comment
@DanielFischer Совершенно верно. Моя точка зрения заключалась в том, что, возможно, мне следовало потратить больше времени на размышления об этом, прежде чем спрашивать сообщество. Я не очень сожалею о своем вопросе, но я узнал о терпении столько же, сколько и о Haskell.   -  person Tientuinë    schedule 27.04.2013
comment
Разобраться в чем-то самому доставляет больше удовольствия, чем быть услышанным? Да, верно. Но если выяснение этого займет слишком много времени, следует спросить. Догадались бы вы сами достаточно скоро, только вы можете сказать, если кто-нибудь.   -  person Daniel Fischer    schedule 27.04.2013


Ответы (1)


Давайте проведем правильный бенчмаркинг. Вот некоторый код, в котором ваш случайный вариант переименован в shuffle1, а мой любимый вариант добавлен как shuffle2.

import System.Random

import Control.Monad

import Control.Monad.ST.Strict
import Data.STRef.Strict

import Data.Vector.Mutable

import Prelude as P

import Criterion.Main


shuffle1 :: RandomGen g => [a] -> g -> ([a], g)
shuffle1 [] g0 = ([],g0)
shuffle1 [x] g0 = ([x],g0)
shuffle1 xs g0 = (x:newtail,g2)
  where (i,g1) = randomR (0, P.length $ P.tail xs) g0
        (xs1,x:xs2) = P.splitAt i xs
        (newtail,g2) = shuffle1 (xs1++xs2) g1


shuffle2 :: RandomGen g => [a] -> g -> ([a], g)
shuffle2 xs g0 = runST $ do
    let l = P.length xs
    v <- new l
    sequence_ $ zipWith (unsafeWrite v) [0..] xs

    let loop g i | i <= 1 = return g
                 | otherwise = do
            let i' = i - 1
                (j, g') = randomR (0, i') g
            unsafeSwap v i' j
            loop g' i'

    gFinal <- loop g0 l
    shuffled <- mapM (unsafeRead v) [0 .. l - 1]
    return (shuffled, gFinal)


main = do
    let s1 x = fst $ shuffle1 x g0
        s2 x = fst $ shuffle2 x g0
        arr = [0..1000] :: [Int]
        g0 = mkStdGen 0
    -- make sure these values are evaluated before the benchmark starts
    print (g0, arr)

    defaultMain [bench "shuffle1" $ nf s1 arr, bench "shuffle2" $ nf s2 arr]

И так, давайте посмотрим некоторые результаты:

carl@ubuntu:~/hask$ ghc -O2 shuffle.hs
[1 of 1] Compiling Main             ( shuffle.hs, shuffle.o )
Linking shuffle ...
carl@ubuntu:~/hask$ ./shuffle 
(1 1,[0, .. <redacted for brevity>])
warming up
estimating clock resolution...
mean is 5.762060 us (160001 iterations)
found 4887 outliers among 159999 samples (3.1%)
  4751 (3.0%) high severe
estimating cost of a clock call...
mean is 42.13314 ns (43 iterations)

benchmarking shuffle1
mean: 10.95922 ms, lb 10.92317 ms, ub 10.99903 ms, ci 0.950
std dev: 193.8795 us, lb 168.6842 us, ub 244.6648 us, ci 0.950
found 1 outliers among 100 samples (1.0%)
variance introduced by outliers: 10.396%
variance is moderately inflated by outliers

benchmarking shuffle2
mean: 256.9394 us, lb 255.5414 us, ub 258.7409 us, ci 0.950
std dev: 8.042766 us, lb 6.460785 us, ub 12.28447 us, ci 0.950
found 1 outliers among 100 samples (1.0%)
  1 (1.0%) high severe
variance introduced by outliers: 26.750%
variance is moderately inflated by outliers

Хорошо, моя система очень шумная, и ее не следует использовать для серьезного бенчмаркинга вещей с похожими числами. Но это вряд ли имеет значение здесь. shuffle2 примерно в 40 раз быстрее, чем shuffle1 в списке из 1001 элемента. Из-за различий между O (n) и O (n ^ 2) это будет только увеличиваться с большими списками. Я уверен, что какой бы ни был ваш тестовый код, это был не алгоритм перемешивания.

На самом деле, у меня есть предположение. Ваша версия достаточно ленива, чтобы постепенно возвращать результаты. 5 секунд — это приемлемый период времени для получения первых нескольких результатов, если вы никогда не прикасаетесь к генератору после его вызова. Может быть, это то, что происходит в вашем времени.

person Carl    schedule 26.04.2013
comment
Спасибо! (И спасибо другим, кто прокомментировал выше). Как я уже сказал, я новичок в Haskell, и, похоже, я просто совершил огромную ошибку новичка и упустил из виду последствия своей лени. Хвост между ног и яйцо на моем лице, по крайней мере, теперь все это имеет для меня смысл. - person Tientuinë; 27.04.2013
comment
@Tientuinë Ну, этому сложно научиться. Надеюсь, вы тоже почерпнете что-то конструктивное из моего ответа. Ознакомьтесь с библиотекой Criterion. Я не назвал это конкретно в своем ответе, но это действительно стоит вашего времени, чтобы изучить его. - person Carl; 27.04.2013
comment
Критерий выглядит довольно полезным, рад, что теперь он у меня на радаре. - person Tientuinë; 27.04.2013