Подтвердить что ты не робот

Почему? (Map digitToInt). шоу` так быстро?

Преобразование неотрицательного Integer в его список цифр обычно выполняется следующим образом:

import Data.Char

digits :: Integer -> [Int]
digits = (map digitToInt) . show

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

Вещи, которые я пытался до сих пор:

Базовая линия:

digits :: Int -> [Int]
digits = (map digitToInt) . show

Получил этот вопрос из другого вопроса в StackOverflow:

digits2 :: Int -> [Int]
digits2 = map (`mod` 10) . reverse . takeWhile (> 0) . iterate (`div` 10)

Попытка бросить свою:

digits3 :: Int -> [Int]
digits3 = reverse . revDigits3

revDigits3 :: Int -> [Int]
revDigits3 n = case divMod n 10 of
               (0, digit) -> [digit]
               (rest, digit) -> digit:(revDigits3 rest)

Этот был вдохновлен showInt в Numeric:

digits4 n0 = go n0 [] where
    go n cs
        | n < 10    =  n:cs
        | otherwise =  go q (r:cs)
        where
        (q,r) = n `quotRem` 10

Теперь эталон. Примечание. Я заставляю оценку с помощью filter.

λ>:set +s
λ>length $ filter (>5) $ concat $ map (digits) [1..1000000]
2400000
(1.58 secs, 771212628 bytes)

Это ссылка. Теперь для digits2:

λ>length $ filter (>5) $ concat $ map (digits2) [1..1000000]
2400000
(5.47 secs, 1256170448 bytes)

Это 3,46 раз больше.

λ>length $ filter (>5) $ concat $ map (digits3) [1..1000000]
2400000
(7.74 secs, 1365486528 bytes)

digits3 4.89 время медленнее. Просто для удовольствия я пытался использовать только revDigits3 и избегать reverse.

λ>length $ filter (>5) $ concat $ map (revDigits3) [1..1000000]
2400000
(8.28 secs, 1277538760 bytes)

Странно, это еще медленнее, 5.24 раза медленнее.

И последний:

λ>length $ filter (>5) $ concat $ map (digits4) [1..1000000]
2400000
(16.48 secs, 1779445968 bytes)

Это 10.43 время медленнее.

У меня создалось впечатление, что только использование арифметики и минусов превосходит все, что связано с преобразованием строк. Видимо, я ничего не могу понять.

Так какой трюк? Почему digits так быстро?

Я использую GHC 6.12.3.

4b9b3361

Ответ 1

Увидев, что я не могу добавлять комментарии, я сделаю немного больше работы и просто проанализирую их все. Я делаю анализ наверху; однако соответствующие данные приведены ниже. (Примечание: все это сделано в 6.12.3, а также - еще нет волшебства GHC 7.)


Анализ:

Версия 1: показать довольно хорошо для ints, особенно тех, которые у нас короткие. Создание струн на самом деле имеет тенденцию быть достойным в GHC; однако чтение строк и запись больших строк в файлы (или stdout, хотя вы не хотели бы этого делать), где ваш код может абсолютно сканировать. Я бы заподозрил, что многие детали, почему это так быстро, вызваны умными оптимизациями в шоу для Ints.

Версия 2:. Это был самый медленный из сгустка при компиляции. Некоторые проблемы: реверс строгий в своем аргументе. Это означает, что вам не удастся выполнить вычисления в первой части списка, пока вы вычисляете следующие элементы; вы должны вычислить их все, перевернуть их, а затем выполнить свои вычисления (а именно (`mod` 10)) в элементах списка. Хотя это может показаться небольшим, это может привести к увеличению использования памяти (обратите внимание на выделенную здесь 5 Гб памяти кучи) и более медленные вычисления. (Короче говоря: не используйте обратную.)

Версия 3: Помните, как я только что сказал, не используйте reverse? Оказывается, если вы его вытащить, то этот снижается до 1,79 с общего времени выполнения - чуть медленнее базовой линии. Единственная проблема заключается в том, что по мере того, как вы углубляетесь в число, вы создаете позвоночник списка в неправильном направлении (по сути, вы входите в "список с рекурсией", в отличие от "на", список).

Версия 4: Это очень умная реализация. Вы выигрываете от нескольких приятных вещей: для одного, quotRem должен использовать алгоритм Евклида, который логарифмичен в своем более крупном аргументе. (Возможно, это быстрее, но я не верю в то, что больше, чем постоянный фактор быстрее, чем Euclid.) Кроме того, вы задерживаетесь в списке, как обсуждалось в прошлый раз, так что вам не нужно разрешать какие-либо списки, как вы go - список уже полностью построен, когда вы возвращаетесь, чтобы разобрать его. Как вы можете видеть, производительность от этого зависит.

Этот код, вероятно, был самым медленным в GHCi, потому что большая часть оптимизаций, выполняемых с флагом -O3 в GHC, справляется с быстрым списком списков, тогда как GHCi не сделает ничего из этого.

Уроки: соответствуют правильному пути в список, следите за промежуточной строгостью, которая может замедлить вычисления, и приготовьтесь к детализации статистики производительности вашего кода. Также скомпилируйте флаги -O3: всякий раз, когда вы этого не сделаете, все те люди, которые много часов набирают в GHC, быстро становятся крупными глазами щенков.


Данные:

Я просто взял все четыре функции, вложил их в один .hs файл и затем изменил по мере необходимости, чтобы отразить используемую функцию. Кроме того, я столкнулся с вашим лимитом до 5e6, потому что в некоторых случаях скомпилированный код работал менее чем за полсекунды на 1e6, и это может начать вызывать проблемы детализации с измерениями, которые мы делаем.

Параметры компилятора: используйте ghc --make -O3 [filename].hs, чтобы GHC сделал некоторую оптимизацию. Мы отправим статистику на стандартную ошибку, используя цифры + RTS -sstderr.

Отбрасывание в -sstderr дает нам вывод, который выглядит так: в случае цифр1:

digits1 +RTS -sstderr
12000000
   2,885,827,628 bytes allocated in the heap
         446,080 bytes copied during GC
           3,224 bytes maximum residency (1 sample(s))
          12,100 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  5504 collections,     0 parallel,  0.06s,  0.03s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    1.61s  (  1.66s elapsed)
  GC    time    0.06s  (  0.03s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    1.67s  (  1.69s elapsed)

  %GC time       3.7%  (1.5% elapsed)

  Alloc rate    1,795,998,050 bytes per MUT second

  Productivity  96.3% of total user, 95.2% of total elapsed

Здесь есть три ключевые статистики:

  • Общая используемая память: только 1 МБ означает, что эта версия очень экономична.
  • Общее время: 1.61s ничего не значит, но мы посмотрим, как он выглядит против других реализаций.
  • Производительность: это всего 100% минус сбор мусора; поскольку мы находимся на уровне 96,3%, это означает, что мы не создаем много объектов, которые мы оставляем лежащими в памяти.

Хорошо, перейдите к версии 2.

digits2 +RTS -sstderr
12000000
   5,512,869,824 bytes allocated in the heap
       1,312,416 bytes copied during GC
           3,336 bytes maximum residency (1 sample(s))
          13,048 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0: 10515 collections,     0 parallel,  0.06s,  0.04s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    3.20s  (  3.25s elapsed)
  GC    time    0.06s  (  0.04s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    3.26s  (  3.29s elapsed)

  %GC time       1.9%  (1.2% elapsed)

  Alloc rate    1,723,838,984 bytes per MUT second

  Productivity  98.1% of total user, 97.1% of total elapsed

Хорошо, поэтому мы видим интересный образец.

  • Одинаковое количество используемой памяти. Это означает, что это довольно хорошая реализация, хотя это может означать, что нам нужно протестировать более высокие входы образцов, чтобы увидеть, можем ли мы найти разницу.
  • Это занимает в два раза больше времени. Мы вернемся к некоторым предположениям относительно того, почему это происходит позже.
  • Это на самом деле немного более продуктивно, но учитывая, что GC не является огромной частью любой программы, это не помогает нам ничего значительного.

Версия 3:

digits3 +RTS -sstderr
12000000
   3,231,154,752 bytes allocated in the heap
         832,724 bytes copied during GC
           3,292 bytes maximum residency (1 sample(s))
          12,100 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  6163 collections,     0 parallel,  0.02s,  0.02s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    2.09s  (  2.08s elapsed)
  GC    time    0.02s  (  0.02s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    2.11s  (  2.10s elapsed)

  %GC time       0.7%  (1.0% elapsed)

  Alloc rate    1,545,701,615 bytes per MUT second

  Productivity  99.3% of total user, 99.3% of total elapsed

Хорошо, поэтому мы видим некоторые странные шаблоны.

  • Мы все еще используем общую память 1 МБ. Таким образом, мы не ударили по какой-либо памяти, неэффективной, что хорошо.
  • Мы не совсем на цифрах1, но у нас есть цифры2, довольно легко.
  • Очень мало GC. (Имейте в виду, что что-то более 95% производительности очень хорошее, поэтому мы не имеем дело с чем-то значительным здесь.)

И, наконец, версия 4:

digits4 +RTS -sstderr
12000000
   1,347,856,636 bytes allocated in the heap
         270,692 bytes copied during GC
           3,180 bytes maximum residency (1 sample(s))
          12,100 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  2570 collections,     0 parallel,  0.00s,  0.01s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    1.09s  (  1.08s elapsed)
  GC    time    0.00s  (  0.01s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    1.09s  (  1.09s elapsed)

  %GC time       0.0%  (0.8% elapsed)

  Alloc rate    1,234,293,036 bytes per MUT second

  Productivity 100.0% of total user, 100.5% of total elapsed

Wowza! Позвольте сломать его:

  • Мы все еще находимся на уровне 1 МБ. Это почти наверняка является особенностью этих реализаций, поскольку они остаются на 1 МБ на входах 5e5 и 5e7. Завещание лени, если вы это сделаете.
  • Мы отрезали около 32% от нашего первоначального времени, что довольно впечатляет.
  • Я подозреваю, что проценты здесь отражают зернистость в мониторинге -sstderr, а не любые вычисления на сверхсветовых частицах.

Ответ 2

Отвечая на вопрос "почему rem вместо мод?" в комментариях. Имея дело с положительными значениями rem x y === mod x y, поэтому единственное рассмотрение примечания - производительность:

> import Test.QuickCheck
> quickCheck (\x y -> x > 0 && y > 0 ==> x `rem` y == x `mod` y)

Итак, какова производительность? Если у вас нет оснований не делать этого (и быть ленивым, это не повод, не знаю Критерий), то используйте хороший инструмент сравнения, я использовал Criterion:

$ cat useRem.hs 
import Criterion
import Criterion.Main

list :: [Integer]
list = [1..10000]

main = defaultMain
        [ bench "mod" (nf (map (`mod` 7)) list)
        , bench "rem" (nf (map (`rem` 7)) list)
        ]

Выполнение этого показывает, что rem заметно лучше, чем mod (скомпилировано с -O2):

$ ./useRem 
...
benchmarking mod
...
mean: 590.4692 us, lb 589.2473 us, ub 592.1766 us, ci 0.950

benchmarking rem
...
mean: 394.1580 us, lb 393.2415 us, ub 395.4184 us, ci 0.950