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

Я новичок в Haskell и пытался создать решатель Scrabble. Он берет буквы, которые у вас есть в настоящее время, находит все их перестановки и отфильтровывает те, которые являются словарными словами. Код довольно прост:

import Data.List

main = do
    dict    <- readFile "words"
    letters <- getLine
    let dictWords = words dict
    let perms = permutations letters
    print [x | x <- perms, x `elem` dictWords]

Однако это невероятно медленно по сравнению с очень похожей реализацией, которую я использую с Python. Есть ли что-то фундаментальное, что я делаю неправильно?

* редактировать: вот мой код Python:

from itertools import permutations

letters = raw_input("please enter your letters (without spaces): ")

d = open('words')
dictionary = [line.rstrip('\n') for line in d.readlines()]
d.close()

perms = ["".join(p) for p in permutations(letters)]

validWords = []

for p in perms:
    if p in dictionary: validWords.append(p)


for validWord in validWords:
    print validWord

Я не засекал их точно, но примерно кажется, что реализация Python примерно в 2 раза быстрее, чем реализация Haskell. Возможно, я не должен был говорить, что код Haskell был «невероятно медленным» по сравнению с ним, но, поскольку Haskell статически типизирован, я думаю, я просто подумал, что он должен был быть намного быстрее, а вовсе не медленнее, чем Python.


person nilcit    schedule 02.09.2016    source источник
comment
Можете ли вы опубликовать код Python и некоторые тесты?   -  person Sage Mitchell    schedule 02.09.2016
comment
words dict — это просто список, а elem выполняет последовательный поиск по списку.   -  person ErikR    schedule 02.09.2016
comment
Строки — это связанные списки в Haskell. Используйте текстовый тип.   -  person Thomas M. DuBuisson    schedule 02.09.2016
comment
Я не уверен, почему за это так сильно проголосовали. Это резонный вопрос для новичка. Здесь на самом деле недостаточно информации, чтобы дать осмысленный ответ, так как многое может зависеть от того, как вы запускаете этот код. Но есть некоторые улучшения высокого уровня, которые вы могли бы внести, например, используя Text и Set. Очень интересен вопрос о том, почему у этого решения производительность отличается от эквивалентного решения Python, и если вы опубликуете свой код Python, это может помочь нам разобраться.   -  person Ian Henry    schedule 02.09.2016
comment
Конечно, ответ «потому что вы используете неправильную структуру данных».   -  person pyon    schedule 02.09.2016
comment
@IanHenry: Вопрос о том, почему ленивые связанные списки работают совсем иначе, чем правильная структура данных словаря, совсем не интересен. (Но, FWIW, я не отрицал этот вопрос. И я бы не стал отрицать вопрос только потому, что он неинтересен.)   -  person pyon    schedule 02.09.2016
comment
@IanHenry спасибо за ваши предложения - как новичок в языке, я очень ценю любую помощь! Я изучил Set, и его использование значительно улучшило производительность. Честно говоря, мне этот код ни для чего не нужен, и меня не особенно волнует время его выполнения; В основном мне было просто любопытно, делаю ли я что-то принципиально неправильное или глупое в Haskell. Помимо использования неправильной структуры данных, можете ли вы сказать, что в моем коде есть что-то еще явно плохое?   -  person nilcit    schedule 02.09.2016
comment
Обязательно прочитайте stackoverflow.com/tags/haskell/info . Жаль, что его как-то не сделали более заметным.   -  person jberryman    schedule 02.09.2016
comment
@ThomasM.DuBuisson, ты, конечно, шутишь. Использование Text здесь не решает фундаментальной проблемы.   -  person dfeuer    schedule 03.09.2016


Ответы (2)


Я новичок в Haskell и пытался создать решатель Scrabble.

Вы можете существенно улучшить ситуацию, используя лучший алгоритм.

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

Вот код, который создает этот словарь как Data.Map. Создание карты связано с начальными затратами, но после первого запроса последующие поиски выполняются очень быстро.

import Data.List
import qualified Data.Map.Strict as Map
import Control.Monad
import System.IO

main = do
  contents <- readFile "words"
  let pairs = [ (sort w, [w]) | w <- words contents ]
      dict = foldl' (\m (k,v) -> Map.insertWith (++) k v m) Map.empty pairs
      -- dict = foldr (\(k,v) m -> Map.insertWith (++) k v m) Map.empty pairs
  forever $ do
    putStr "Enter letters: " >> hFlush stdout
    letters <- getLine
    case Map.lookup (sort letters) dict of
      Nothing -> putStrLn "No words."
      Just ws -> putStrLn $ "Words: " ++ show ws

Время создания карты для ворд-файла размером 236К слов (2,5 МБ) составляет около 4-5 секунд. Более высокая производительность, вероятно, возможна при использовании ByteStrings или Text вместо Strings.

Несколько хороших комбинаций букв, которые стоит попробовать:

steer rat tuna lapse groan neat

Примечание. Используя GHC 7.10.2, я обнаружил, что этот код работает лучше без компиляции с параметром -O2.

person ErikR    schedule 02.09.2016
comment
Большое спасибо за Ваш ответ! На самом деле я экспериментировал с решением, очень похожим на то, которое вы предоставили, - сортировкой ввода и слов из словаря и таким образом проверяя анаграммы. Я использовал структуру Set и проверил членство с помощью функции Set.member. Эта реализация на самом деле не очень сильно улучшила мое время работы. Однако ваша реализация после инициализации невероятно быстра! Я обязательно изучу карту. Еще раз спасибо за ваш вклад - как новичок в языке, я очень благодарен за помощь! - person nilcit; 02.09.2016
comment
В качестве продолжения - когда я включил в свой код вечную строку (та, где я сортировал ввод и словарные слова), запросы после первого были мгновенными. Я думаю, это из-за ленивой оценки? Как в коде действительно не создает словарь до первого запроса, когда он действительно нужен, но после того, как он уже есть для последующих? - person nilcit; 02.09.2016
comment
Вот так. Однако вы должны быть осторожны с forever и версией и параметрами компилятора - иногда карта пересчитывается для каждой итерации. Когда карта не пересчитывается, второй и последующие поиски выполняются мгновенно. - person ErikR; 02.09.2016
comment
Хотя это может быть достаточно быстро для этой работы, Data.Map является довольно плохой структурой данных, если вы используете строки (в любом формате) в качестве ключей. HashMap, вероятно, было бы лучше, но что-то более похожее на trie, вероятно, лучше. - person dfeuer; 03.09.2016

Проверка, является ли x элементом dictWords, вероятно, будет очень медленной. Я бы предположил, что ваша аналогичная реализация Python хранит dictWords в наборе или отсортированном векторе (в последнем случае с использованием двоичного поиска)? Похоже, вы, вероятно, хотите сделать то же самое здесь.

Используя этот список слов и приведенный ниже код, версия Python запускается примерно за 30 секунд. , а версия на Haskell — 1,5 минуты. Таким образом, Haskell медленнее (возможно, потому, что он использует связанный список, который при прочих равных условиях медленнее перебирает), но я бы не назвал его «невероятно медленным» по сравнению с Python. Переключение на использование набора в любой версии сокращает время до менее 1 секунды.

from itertools import permutations
f = open('twl06.txt')
words = f.read().split()

print [''.join(p) for p in permutations('apricot') if ''.join(p) in words]

А вот код на Haskell, основанный на наборах:

import Data.Set
import Data.List

main = do
    dict    <- readFile "twl06.txt"
    let letters = "apricot"
    let dictWords = Data.Set.fromList $ words dict
    let perms = permutations letters
    print [x | x <- perms, member x dictWords]
person happydave    schedule 02.09.2016
comment
Код Python хранит словарь в виде списка строк, как и реализация Haskell. В python для проверки членства я использую функцию in - person nilcit; 02.09.2016
comment
Хм, тогда я не знаю четкого ответа на ваш вопрос, но сохранение dictWords в виде набора по-прежнему, вероятно, решит вашу проблему во время выполнения. - person happydave; 02.09.2016
comment
Думаю, я ожидал, что Haskell будет быстрее, чем Python, поскольку он статически типизирован, поэтому, когда он в 3 раза медленнее, я назвал это невероятно медленным. Это была плохая формулировка с моей стороны, я должен был яснее объяснить ситуацию. Другие также предложили использовать Set, и это определенно улучшило время работы. Однако мне все еще любопытно, почему списки в Haskell работают медленнее, чем списки в Python. Для реализации Python, чтобы найти слово, ему все еще нужно выполнить итерацию по списку. Являются ли списки Python более оптимизированными, чем списки Haskell? Как можно так ускорить итерацию? - person nilcit; 02.09.2016
comment
@nilcit Обратите внимание, что python list являются встроенными, что означает, что они реализованы непосредственно в C как массивы с изменяемым размером. Это означает, что один вызов element in sequence будет стоить интерпретируемых накладных расходов одного вызова метода, затем реализация для list.__contains__ включится и выполнит C-цикл над базовым массивом и вызовет оператор равенства из C. так что в конце CPython in на самом деле не имеет таких больших накладных расходов по сравнению с скомпилированными языками, потому что большая часть работы выполняется в скомпилированном коде, а единственными накладными расходами являются общие сравнения. - person Bakuriu; 02.09.2016