LINUX.ORG.RU

Haskell медленно обрабатывает файлы

 , ,


0

4

Прочитав книгу Курта, решил реализовать AES на Haskell в качестве первой практики. Сначала была эйфория от красоты конструкций и языка, потом агония, когда картинку несколько мегабайт программа шифровала целую минуту, хоть и сделала все правильно и хеши при расшифровке совпали. На больших файлах запускать страшно, боюсь не выдержит, хотя в конечном итоге хочу оптимизировать именно для них.


import Data.Binary
import Data.Bits(xor)
import qualified Data.ByteString.Lazy as BSL
import Data.Int(Int64)
import System.Environment


mask :: BSL.ByteString
mask = BSL.concat $ map encode numbers
  where numbers = [1..] :: [Int64]

ctrMode :: BSL.ByteString -> BSL.ByteString
ctrMode = BSL.packZipWith xor mask

main :: IO ()
main = do
  (inputFile:_) <- getArgs
  let outputFile = "ctr_" ++ inputFile 

  inputContent <- BSL.readFile inputFile
  let newContent = ctrMode inputContent

  BSL.writeFile outputFile newContent

Данный код реализует Counter Mode для обработки открытого текста перед шифрованием, чтобы на одинаковых блоках зашифрованный текст был разный.

$ ghc -O3 ctr.hs
$ dd if=/dev/zero of=./zeros bs=1M count=1536
$ time ./ctr zeros
./ctr zeros  86,75s user 7,54s system 100% cpu 1:34,14 total
$ time openssl enc -aes-128-cbc -in zeros -out enc_zeros
openssl enc -aes-128-cbc -in zeros -out enc_zeros  1,31s user 3,50s system 66% cpu 7,187 total

Файл 1.5 гигабайта обрабатывается за 90 секунд, и делает лишь xor, в то время как openssl тот же файл за пару секунд полностью шифрует. Программа собрана с -O3. Как можно оптимизировать?

Я уверен, что проблема в маленьких чанках при чтении файла. Как их можно увеличить? hGetContent работает с дефолтным чанком. hGetContentN, которая принимает размер чанка, не экспортируется (https://hackage.haskell.org/package/bytestring-0.11.1.0/docs/src/Data.ByteString.Lazy.html#hGetContents). Вручную писать считывание с большими чанками? Как-то некрасиво, да и не знаю как, и это похоже на что-то базовое, что должно быть в стандартной библиотеке, но я не могу найти.



Последнее исправление: jumminy1968 (всего исправлений: 1)

Там есть hGet, который берёт размер на чтение. Если ты хочешь руками всё сделать, то используй эту функцию.

А вообще, для того, что ты делаешь, обычно используют либо Pipes, либо Conduit. Это две библиотеки с примерно одинаковым функционалом для организации поточной обработки данных.

Проблема два: ты зачем-то генерируешь список Int64, после чего каждый элемент конвертируешь в байты и делаешь xor с байтами данных по одному за раз. Так не делается. Тебе надо блоками это делать всё, так что лучше посмотри в сторону пакета vector.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 3)
Ответ на: комментарий от hateyoufeel

Спасибо про Pipes, сейчас буду читать.

генерируешь список Int64, после чего каждый элемент урезаешь до байта

В моём контексте encode имеет аннотацию Int64 -> ByteString, и в полученном ByteString будет 8 элементов

ghci> take 30 (BSL.unpack mask)
[0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,0,0,0,0,0,0]

Вывод ghci именно такой и задумывался, и вроде ничего не урезается до байтов, до распаковки все операции проводятся исключительно над ByteString.

xor с байтами данных по одному за раз

А как можно сделать xor блоками? Я знаю только про функцию Word8 -> Word8 -> Word8, а встроенной для блоков не знаю, и если самому делать, всё равно ведь будет поэлементно обрабатываться. И то я использовал zipWith не из Prelude, а из BSL, вдруг помогло бы

jumminy1968
() автор топика
Ответ на: комментарий от jumminy1968

А как можно сделать xor блоками? Я знаю только про функцию Word8 -> Word8 -> Word8, а встроенной для блоков не знаю,

Встроенной и нет, но есть из пакета largeword. Word256 оттуда – это похоже на то, что ты хочешь.

https://hackage.haskell.org/package/largeword-1.2.5/docs/Data-LargeWord.html

Навскидку, алгоритм у тебя примерно такой вырисовывается: считываешь из файлы блок данных (Vector Word256), делаешь над ним xor, пишешь в другой файл, повторяешь пока данные не кончатся. Размер вектора меняй по вкусу, пока не подберёшь оптимальную производительность. Я бы начал с размера сектора на диске (4kb, т.е. 32 x Word256). В принципе, можешь и без pipes/conduit это сделать. Должно быть не слишком сложно.

В качестве бонуса, когда сделаешь всё выше, можешь попробовать параллельную работу с элементами вектора приделать через parallel (https://hackage.haskell.org/package/parallel). Не факт, что сильно ускорит, но попытаться можно.

P.S. есть ещё пакет Crypto с реализацией AES, и там аналогично LargeWord используется. Можешь туда в код подсмотреть.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 3)

Классическая криптография, как правило, такая байтодрочка, что никакого толка для неё от языковой функциональщины нет.

seiken ★★★★★
()

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

Ну так хаскель - это больше про корректность и надежность, чем про производительность. Книжка Курта неплоха для знакомства с основами языка, но оптимизировать узкие места она конечно не учит. Если ты хочешь, чтобы хаскельный код работал так же быстро, как на Си, то и писать надо примерно как на Си. Ну то есть вплоть до дрочки указателей и peek/poke. Если производительность варианта с Vector Word256 не устроит, то можешь для интереса глянуть библиотечку xor:

https://github.com/haskell-hvr/xor

Она правда делает не совсем то, что тебе нужно, а именно ксорит Word8 или Word32 с байтстрингом, а не два байтстринга. Зато производительность заявляется «как на Си».

archie
()

Я решил сделать следующее:

  • считывать файл энергичными ByteString по чанкам
  • конвертировать ByteString в Data.Vector.Storable Word8 и обратно без копирования (библиотеки vector, bytestring-to-vector)
  • Vector.Storable Word8 можно за O(1) перевести в Vector Word32 или Vector Word64 с помощью unsafeCast
  • проводить операцию xor на Word64, а не Word8, полученном с помощью пункта выше

Итого получилось файл 1.5 Гб перевести в ctrMode за ~25 секунд. Быстрее думаю на чистом Haskell уже нельзя, только если прикреплять Си. Теперь осталось сам шифр оптимизировать, а не обработку файла.

Всем большое спасибо за ответы, вы очень помогли

jumminy1968
() автор топика