о циклах в IO

3 views
Skip to first unread message

nsk.coder

unread,
Jun 26, 2009, 10:35:53 AM6/26/09
to SPb Haskell User Group
Прошу помочь советом в следующем.

Мне нужно (для некоторой не относящейся к делу цели) создать большой
файл (от 100 мегабайт до неск. гигабайт), напр. для простоты забитый
одними двоичными нулями.

Т.е. итеративно пишем в файл двоичный ноль или строку, содержащую
'\x00' нужное количество раз.

Изначально я сделал это на Perl и на C. Потом стал использовать данную
задачу как некий тест, написал аналогичные программки на Lua и на
Haskell.

Получились следующие тайминги для генерации 100 мегабайтного файла:

1. C 22 сек
2. Perl 53 сек
3. Lua 83.766 сек
4. Haskell (компилир) 100.484375 сек
5. Haskell (runghc) 110.765625 сек

Я в ужасе. Как я понимаю, в моем случае -- проблема в неумении
работать с организацией циклов в монаде IO. Намеренно говорю о циклах,
хотя написан код как рекурсия. Видимо, при организации императивного
алгоритма уместно думать в терминах все-таки циклов.

Вопрос, что я сделал плохо, как сделать хорошо?
Мало того, что работает медленно, так еще и громоздко!
(я скажу, что первые версии без буферизации и т.п. вообще работали по
800 сек!)
Или так и должно быть? У меня опыта маловато...

Привожу листинг на Haskell:

import System.IO
import System.CPUTime
main = do
t1 <- getCPUTimeDouble
print t1
let n = 1024*1024*100+1
let f = "white100"
h <- openFile f WriteMode
hSetBuffering h (BlockBuffering (Just (1024)))
b <- hGetBuffering h
-- print b
loop n h
hClose h

t2 <- getCPUTimeDouble
print t2
print (t2 - t1)

loop :: Integer -> Handle -> IO()
loop 0 h = return ()
loop i h = do
hPutChar h '\x00'
loop (i-1) h

getCPUTimeDouble :: IO Double
getCPUTimeDouble = do t <- System.CPUTime.getCPUTime; return
((fromInteger t) * 1e-12)


Ну и для сравнения листинг на Perl

#!/usr/bin/perl -w

$n = 100*1024*1024; # число байт

open (WRITEFILE, '>white100') or die "Can't open writefile: $!";
binmode(WRITEFILE);

$t1 = time();
print "\n".$t1;

for($i=0;$i<$n;$i++) { print WRITEFILE "\x00"; };

$t2 = time();
print "\n".$t2." ".($t2-$t1);
close;

P.S. подскажите, как отвечать в обсуждениях? Я отправил ответ
("Автору", больше вариантов кроме "Переслать", не нашел) -- в теме "о
как узнать сколько времени выполняется код?" Но так и не увидел своего
ответа, в чем дело?

Denis Moskvin

unread,
Jun 26, 2009, 10:45:44 AM6/26/09
to spb...@googlegroups.com
26 июня 2009 г. 17:35 пользователь nsk.coder (nsk....@gmail.com) написал:

>
> P.S. подскажите, как отвечать в обсуждениях? Я отправил ответ
> ("Автору", больше вариантов кроме "Переслать", не нашел) -- в теме "о
> как узнать сколько времени выполняется код?" Но так и не увидел своего
> ответа, в чем дело?

У нас стоит премодерация для новых участников из-за обилия спама. Ваше
сообщение ее успешно прошло - теперь можете участвовать в обсуждении
:)

Maxim Taldykin

unread,
Jun 26, 2009, 10:59:04 AM6/26/09
to spb...@googlegroups.com
К чему такие сложности?

Исходный вариант на моей машинке работает 22 секунды.
А вот такой всего три с половиной:

main = do
t1 <- getCPUTimeDouble
print t1
let n = 1024*1024*100+1
let f = "white100"

writeFile f $ replicate n '\x00'

t2 <- getCPUTimeDouble
print t2
print (t2 - t1)


Собирать лучше с оптимизацией: ghc --make -O2 ...


26 июня 2009 г. 18:35 пользователь nsk.coder (nsk....@gmail.com) написал:

nsk.coder

unread,
Jun 30, 2009, 6:35:11 AM6/30/09
to SPb Haskell User Group
А почему я не могу отвечать в других темах, а только в этой?? :-(
(В других темах я не нашел кнопки "Ответить", только в этой, своей
теме..)

nsk.coder

unread,
Jun 30, 2009, 6:48:14 AM6/30/09
to SPb Haskell User Group
Большое спасибо за совет!

1. Действительно, при указанных флагах оптимизации на моей машине
исходная программа прошла за 56 сек, а Ваш вариант программы -- за 11
(быстрее неоптимизированного вариант на С !!)

2. Идея использования replicate мне в голову не пришла вообще -- очень
удивился эффективности!

3. Однако, решив одну проблему (с большой строкой одинаковых
символов), я бы хотел перепостановить вопрос так:

Хорошо, пусть мне надо не повторять один и тот же символ, а, скажем,
считывать побайтно огромный файл (и даже не весь, а фиксированное
число сотен мегабайт или гигабайт), делать обработку каждого байта
какой-то функцией и записывать байт-результат последовательно в другой
файл.

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

В данном случае, Вы очень красиво учли специфику первой задачи и
возможность оптимизации.

А в общем случае? Если монада IO -- это "островок императивного мира",
то должно быть эффективное средство делать цикл внутри нее??

Maxim Taldykin

unread,
Jun 30, 2009, 7:03:58 AM6/30/09
to spb...@googlegroups.com
30 июня 2009 г. 14:48 пользователь nsk.coder (nsk....@gmail.com) написал:
> [...]

>  Хорошо, пусть мне надо не повторять один и тот же символ, а, скажем,
> считывать побайтно огромный файл (и даже не весь, а фиксированное
> число сотен мегабайт или гигабайт), делать обработку каждого байта
> какой-то функцией и записывать байт-результат последовательно в другой
> файл.

main = readFile "in.txt" >>= writeFile "out.txt" . map f
f = chr . (+1) . ord -- обработка байта

Идея заключается в использовании результатов ленивых функций как итераторов.
Т.е. readFile "in.txt" возвращает не содержимое файла, а итератор по нему.

А как быть с циклами в IO я и сам не знаю. Не часто доводилось такое делать.

Dmitry Olshansky

unread,
Jun 30, 2009, 7:46:58 AM6/30/09
to spb...@googlegroups.com
Если не ошибаюсь, если файл бинарный, то с readFile - writeFile будут проблемы. 
Тогда надо более низкоуровнево читать/писать.

Циклы все равно не потребуются.

Насчет рекурсии - как я понимаю, глубина рекурсии map равна где-то двум. Во всяком случае, она не зависит от величины списка.

30 июня 2009 г. 15:03 пользователь Maxim Taldykin <jor...@gmail.com> написал:

nsk.coder

unread,
Jul 24, 2009, 12:17:32 PM7/24/09
to SPb Haskell User Group
Вновь спасибо!

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

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

On 30 июн, 18:03, Maxim Taldykin <jor...@gmail.com> wrote:
> 30 июня 2009 г. 14:48 пользователь nsk.coder (nsk.co...@gmail.com) написал:

nsk.coder

unread,
Jul 24, 2009, 12:42:48 PM7/24/09
to SPb Haskell User Group
Да, действительно, проблема с бинарными файлами в этом решении есть --
специально проверил при работе с jpeg-файлом (проблема решается
функцией openBinaryFile и соотв. функций чтения/записи из System.IO)

А можно уточнить, что понимается под "глубиной рекурсии равной 2" ??

Судя по оперделению в Prelude -- map определяется рекурсивно, и тогда
конечно же должен прямо зависеть от длины списка.

В примере Maxim'a Taldykin'a, как я понимаю, эффективность достигается
за счет ленивости -- и то, это вроде касается скорости и использования
памяти для хранения списка (т.е. один файл лениво читается, а в другой
-- лениво сбрасывается, весь список в памяти держать не надо)

Интересно, а если гигантский список придется держать в памяти?

Ну и кроме того, при рекурсии ведь нужно держать в памяти предыдущие
незаконченные вычисления -- а если таковых на гигабайты?

Все равно соменения остаются и есть ощущение в необходимости
огранизации циклов...

Dmitry Olshansky

unread,
Aug 3, 2009, 4:09:26 PM8/3/09
to spb...@googlegroups.com
Про "глубину рекурсии" я ерунду написал. Имел в виду, что не будет вложенных вызовов map из map.
В рекурсии самой по себе никакой проблемы нет. Это универсальный способ определения функции. Реализация языка может плохо воспринимать рекурсию. В Haskell это не так, поскольку описание функции не определяет порядок ее вычисления, в отличие от императивного языка. 

Есть некоторая проблема с foldl - см. книжку Real World Haskell. 


24 июля 2009 г. 20:42 пользователь nsk.coder <nsk....@gmail.com> написал:
Reply all
Reply to author
Forward
0 new messages