Рекомендации алгоритма Haskell и предложения по альтернативным решениям
Я оставляю описание проблемы ниже, но я редактирую все несоответствующие биты, когда я иду.
1) Благодаря dfeuer, я смог полностью избавиться от этой программы. Я дошел до 9.2 секунд с 10.1.
2) Благодаря модификации того, что SO user приложение опубликовано, я смог сэкономить до 7,0 секунд, сделав IO как очень эффективный.
3) Благодаря другой версии groupBy нашла здесь, теперь я нахожусь на 6.2 секунды.
4) Повторная реализация выходного файла построителя и теперь через 5.8 секунды.
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy.Char8 as DB
import qualified Data.Function as DF
import Control.Applicative
import Data.Monoid
import System.IO
groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
groupBy rel [] = []
groupBy rel (x:xs) = (x:ys) : groupBy rel zs
where (ys,zs) = groupByAux x xs
groupByAux x0 (x:xs)
| rel x0 x = (x:ys, zs)
where (ys,zs) = groupByAux x xs
groupByAux y xs = ([], xs)
filterLengthBy :: (Int -> Bool) -> [[a]] -> [[a]]
filterLengthBy fn = filter (flb fn)
where flb fn xs = fn $ length xs
buildOutput x y = BB.intDec x <> BB.char7 ' ' <> BB.intDec y <> BB.char7 '\n'
tensAndHundreds :: [[[Int]]] -> [BB.Builder]
tensAndHundreds [] = []
tensAndHundreds (x:xs)
| length x /= 10 = tens x ++ tensAndHundreds xs
| otherwise = buildOutput (head $ head x) 100 : tensAndHundreds xs
tens :: [[Int]] -> [BB.Builder]
tens = foldr (\x acc -> buildOutput (head x) 10 : acc) []
dbIntVals :: DB.ByteString -> [Int]
dbIntVals xs =
case (DB.readInt xs) of
Just (x', xs') -> x' : dbIntVals (DB.tail xs')
Nothing -> if xs == DB.empty
then []
else dbIntVals (DB.tail xs)
generateResults :: [Int] -> [BB.Builder]
generateResults xs = tensAndHundreds
$ groupBy ((==) `DF.on` (`quot` 100) . head)
$ filterLengthBy (== 10)
$ groupBy ((==) `DF.on` (`quot` 10)) xs
displayResults :: [BB.Builder] -> IO ()
displayResults xs = BB.hPutBuilder stdout $ mconcat xs
main :: IO ()
main = DB.getContents >>=
displayResults . generateResults . dbIntVals
Мои два вопроса, которые были в нижней части исходного сообщения:
-
Учитывая подход, который я опубликовал выше, есть ли что-то, что я пропустил, что повысит эффективность и, следовательно, производительность этой программы? Является ли выделение пространства окончательным узким местом? Другими словами, прошел ли метод groupBy?
-
Какие еще способы я мог бы рассмотреть эту проблему, которая повысила бы эффективность, все еще пытаясь избежать счётчиков в традиционном смысле? Я еще не знаком с продвинутыми функциями Haskell, но, пожалуйста, не стесняйтесь предлагать указатели.
Это оригинальная проблема. Я оставил первую версию кода, но удалил все материалы профайлера
Я начал изучать Haskell (снова) и решил попробовать некоторые случайные проблемы, которые я нашел. Одна из проблем, которые я разработал на форумах Arch Linux, выглядит следующим образом:
Учитывая набор уникальных, восходящих, но не обязательно непрерывных целых чисел, для каждого потенциального поддиапазона 100, начинающегося с 0 (0-99, 100-199, 1300-1399 и т.д.), если этот субдиапазон имеет все 100 элементов, напечатайте начальное значение, а затем 100:
0 100
145600 100
В противном случае для каждого непрерывного набора 10 в том же диапазоне, начиная с 0 (0-9, 10-19, 15430-15439 и т.д.), напечатайте головку этого диапазона, а затем 10:
30 10
70 10
145620 10
145650 10
Пример вывода из крошечного набора тестов выглядит следующим образом:
0 10
19000 100
20320 10
54540 100
В моем основном тестовом файле 56,653,371 ints, из которых 290,197 наборов из десяти и четырех наборов из 100. Тест-машина оснащена процессором AMD 1090T и 8 ГБ ОЗУ и работает с GHC 7.8.3 на 64-разрядном Linux, Единственными используемыми флагами компилятора являются "-O2 -fflvm".
Есть одна важная вещь, которую нужно отметить: я намеренно избегаю использования счетчиков, которые часто используются в программировании. Например, имея счетчики, которые отслеживают 10 и 100: "если счет один == 100, то...". Я бы хотел избежать отслеживания состояния этих счетчиков и использовать только доступные функции /HOF.
Вот моя лучшая попытка. Завершает задачу за 10,1 секунды (в настоящее время на 5,8 секунды). Чтобы представить это в перспективе, версия C из потока, который я упомянул, заканчивается через 9.4 секунды. Там еще одна версия Haskell, которую я не писал, использует счетчики, которые завершают ее на 8.1 секунды ( в настоящее время на 4.7). Это мое вдохновение (спасибо Xyne за это и преподавание, где заявления о случаях приходят в их собственные).
** РЕДАКТИРОВАТЬ **
Я забыл упомянуть, что он делает. Он разбивает входные данные с использованием межсекторного деления на потенциальные диапазоны в десять и помещает их в список списков (отредактированный для уточнения расщепления luqui). Итак:
1 4 7 9 -> [1,4,7,9]
1 2 3 8 -> [1,2,3,8]
0 1 2 3 4 5 6 7 8 9 -> [0,1,2,3,4,5,6,7,8,9]
7 8 9 10 11 12 13 14 15 16 17 18 19 20 25 27 -> [7,8,9] [10,11,12,13,14,15,16,17,18,19] [20,25,27]
filterLengthBy затем удаляет все, что не является длиной 10, потому что оно нам не нужно. Полученный список разбивается на списки списков, основанные на целочисленном делении головы. Итак:
[[0,1,2,3,4,5,6,7,8,9],[10,11,12,13,14,15,16,17,18,19],[100,101,102,103,104,105,106,107,108,109]]
становится:
[[[0,1,2,3,4,5,6,7,8,9],[10,11,12,13,14,15,16,17,18,19]],[[100,101,102,103,104,105,106,107,108,109]]]
Если второй уровень длины списка равен 10, то соответствует набор из 100 и выводится на набор решений. В противном случае этот список имеет списки из 10 и должен быть индивидуально добавлен к набору решений.
Старая версия кода
import qualified Data.ByteString.Lazy.Char8 as DB
import qualified Data.Function as DF
import qualified Data.List as DL
import Control.Applicative
groupByEqOn :: Eq b => (a -> b) -> [a] -> [[a]]
groupByEqOn fn = DL.groupBy ((==) `DF.on` fn)
filterLengthBy :: (Int -> Bool) -> [[a]] -> [[a]]
filterLengthBy fn = filter (flb fn)
where flb fn xs = fn $ length xs
tensAndHundreds :: [[[Int]]] -> [(Int, Int)]
tensAndHundreds [] = []
tensAndHundreds (x:xs)
| length x /= 10 = tens x ++ tensAndHundreds xs
| otherwise = (head $ head x, 100) : tensAndHundreds xs
tens :: [[Int]] -> [(Int, Int)]
tens = map (\x -> (head x, 10))
dbEmpty :: DB.ByteString
dbEmpty = DB.empty
dbIntVals :: [DB.ByteString] -> [Int]
dbIntVals [] = []
dbIntVals (x:xs) =
case (DB.readInt x) of
(Just (x', dbEmpty)) -> x' : dbIntVals xs
_ -> error "*** Error: Non-integer input ***"
generateResults :: [Int] -> [(Int, Int)]
generateResults xs = tensAndHundreds
$ groupByEqOn ((`quot` 100) . head)
$ filterLengthBy (== 10)
$ groupByEqOn (`quot` 10) xs
displayResults :: [(Int, Int)] -> IO ()
displayResults = mapM_ (\(a, b) -> putStrLn (show a ++ " " ++ show b))
main :: IO ()
main = dbIntVals <$> DB.lines <$> DB.getContents >>=
displayResults . generateResults
Единственные две вещи, которые удивляют меня от профилировщика, - это в первую очередь то, насколько быстро выполняется filterLengthBy. Главным удовольствием для всех людей, участвующих в получении фильтра, было так быстро. Это действительно замечательная вещь, учитывая, сколько данных я выбрал. В этом отношении замечательно, что то, что я написал, так же быстро, как и есть. Другой сюрприз - это то, насколько медленны dbIntVals. Я действительно выполнял правильную проверку ошибок, и это делает вещи немного медленнее, но я все еще удивляюсь. Кроме этого, он, похоже, читается точно так же, как мой код.
Ответы
Ответ 1
Я в значительной степени делаю эту тему. Я пришел сюда, чтобы узнать, что может предложить сообщество SO Haskell, и я получил полезные ответы. За это я благодарен.
Последняя версия кода, который я опубликовал, показывает, что, учитывая хороший код, GHC может создавать быстрые, точные программы, используя только функциональные идиомы. Это может показаться очевидным, но это первая программа Haskell, которую я когда-либо писал с нуля (с очевидной копией и вставкой groupBy).
Что я узнал?
Из dfeuer мне напомнили о том, чтобы быть осторожным с временами, когда полагаться только на встроенную функцию, такую как карта, возможно, не самый лучший подход. Иногда полезно комбинировать логику. В этом случае я получил очень значительное увеличение производительности.
Из приложения я узнал об эффективном выходе. Увеличение производительности было потрясающим.
Если какой-нибудь разработчик Haskell/GHC читает это:
На мой взгляд, groupBy здесь должен быть включен в стандартную библиотеку и не отброшен в Hackage. Зачем? Он работает быстро, сохраняя точность. Для той же операции над миллионами элементов и с получением одинаковых результатов было увеличение производительности на 8,2%. Я понимаю, что он не делает то же самое; Я все еще думаю, что стоит взглянуть.
Действительно ли это действительно имеет смысл? Я видел увеличение производительности на 10% по сравнению с тем же набором данных, что и выше, с точностью 100%. Я понимаю, что "rem" и "mod" делают разные вещи и что они имеют тенденцию крутиться в разных "направлениях", но первый из них выполняет это по огромным полям. Вероятно, я не понимаю этого, но 10% довольно сложно игнорировать.