You are viewing gaperton

June 2014   01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Быстрый файловый ввод-вывод в Erlang

Posted on 2008.01.06 at 01:03
Tags: , ,
Думаете, такого не бывает, да? :) Сейчас мы увидим, что будет, если полагаться на сильные стороны платформы, а не на слабые :). Мы сделаем самое быстрое в мире на момент публикации построчное чтение текстовых файлов для Эрланга.

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

Вскрытие показало, что ацки медленно работает исключительно модуль io, на что и налетают новички, используя функции io форматированного воода-вывода, скажем, для чтения файла по строкам. Но не модуль file. Причина в том, что у file есть два режима работы - raw (в котором дескриптор файла является идентификатором драйвера, дающего прямой интерфейс к функциям ввода-вывода оперсистемы), и обычный, когда создается оберточный процесс, PID-ом которого и является дескриптор файла.


Чего еще хорошего есть в режиме raw? То, что в нем можно указать опцию binary, и в этом случае не будет происходить копирования данных при чтении и записи, потому что binaries длиннее 64 байт хранятся в разделяемом хипе. Так и надо делать в том случае, если требуется производительность.

Если не указать binary, то ввод-вывод будет возвращать прочитанные данные в виде списков байт. Что в 8 (восемь) раз больше по объему, плюс - ЭТО еще и копируется между процессами. А если вы кроме этого не укажете raw, то копироваться данные будут два раза.

Кроме того, можно при открытии файла в любом из режимов указать ему опции read_ahead и delayed_write. Это приводит к выделению буфера внутри драйвера file, и системные вызовы будут дергаться реже. Кроме того, очень сильно ускорится последовательное чтение/запись - диск будет меньше дергать головки.

Чем плох режим raw? Тем, что файл открыый в этом режиме может быть использован только процессом, который его открыл, и только на том узле, где существует данный файл. Оказывается, если пользоваться io и стандартным режимом, файлы доступны на любом узле кластера, и неважно, разделяются у них диски, или нет. Прикольно, правда? Еще тем, что вам доступны только тупые функции file:read и file:write, форматированный ввод-вывод из io работать не будет.

Так вот, потеря на мой взгляд невелика, поэтому протестируем режим raw. А именно, я напишу для этого режима эффективную реализацию get_line. После чего проверим, с какой скоростью я буду читать файл длиной 821 мегабайт.

Идея следующая. Читаем в raw-режиме данные блоками фиксированной длины. Естественно, в режиме [ raw, binary ], чтобы избежать копирования. После чего, выделяем в блоках строки, поиском символа конца строки и выделением под-бинариса. Что также не приводит к копированию данных. Когда буфер кончится, считаем новый. И так, пока не кончится файл.

Для начала нам потребуется функция, которая будет быстро-быстро искать заданный символ в бинарисе. Выглядит она так:
find_8( Buffer, Char ) -> find_8( Buffer, Char, 0 ).
find_8( Buffer, Char, Pos ) ->
	case Buffer of
		<< _:Pos/bytes, Char:8, _/bytes >> -> Pos; 
		<< _:Pos/bytes, _:1/bytes, Char:8, _/bytes >> -> Pos + 1;
		<< _:Pos/bytes, _:2/bytes, Char:8, _/bytes >> -> Pos + 2;
		<< _:Pos/bytes, _:3/bytes, Char:8, _/bytes >> -> Pos + 3;
		<< _:Pos/bytes, _:4/bytes, Char:8, _/bytes >> -> Pos + 4;
		<< _:Pos/bytes, _:5/bytes, Char:8, _/bytes >> -> Pos + 5;
		<< _:Pos/bytes, _:6/bytes, Char:8, _/bytes >> -> Pos + 6;
		<< _:Pos/bytes, _:7/bytes, Char:8, _/bytes >> -> Pos + 7;
		<< _:Pos/bytes, _:8/bytes, Char:8, _/bytes >> -> Pos + 8;
		<< _:Pos/bytes, _:9/bytes, Char:8, _/bytes >> -> Pos + 9;
		<< _:Pos/bytes, _:10/bytes, Char:8, _/bytes >> -> Pos + 10;
		<< _:Pos/bytes, _:11/bytes, Char:8, _/bytes >> -> Pos + 11;
		<< _:Pos/bytes, _:12/bytes, Char:8, _/bytes >> -> Pos + 12;
		<< _:Pos/bytes, _:13/bytes, Char:8, _/bytes >> -> Pos + 13;
		<< _:Pos/bytes, _:14/bytes, Char:8, _/bytes >> -> Pos + 14;
		<< _:Pos/bytes, _:15/bytes, Char:8, _/bytes >> -> Pos + 15;
		<< _:Pos/bytes, _:16/bytes, Char:8, _/bytes >> -> Pos + 16;
		<< _:Pos/bytes, _:17/bytes, Char:8, _/bytes >> -> Pos + 17;
		<< _:Pos/bytes, _:18/bytes, Char:8, _/bytes >> -> Pos + 18;
		<< _:Pos/bytes, _:19/bytes, Char:8, _/bytes >> -> Pos + 19;
		<< _:Pos/bytes, _:20/bytes, Char:8, _/bytes >> -> Pos + 20;
		<< _:Pos/bytes, _:21/bytes, Char:8, _/bytes >> -> Pos + 21;
		<< _:Pos/bytes, _:22/bytes, Char:8, _/bytes >> -> Pos + 22;
		<< _:Pos/bytes, _:23/bytes, Char:8, _/bytes >> -> Pos + 23;
		<< _:Pos/bytes, _:24/bytes, Char:8, _/bytes >> -> Pos + 24;
		<< _:Pos/bytes, _:25/bytes, Char:8, _/bytes >> -> Pos + 25;
		<< _:Pos/bytes, _:26/bytes, Char:8, _/bytes >> -> Pos + 26;
		<< _:Pos/bytes, _:27/bytes, Char:8, _/bytes >> -> Pos + 27;
		<< _:Pos/bytes, _:28/bytes, Char:8, _/bytes >> -> Pos + 28;
		<< _:Pos/bytes, _:29/bytes, Char:8, _/bytes >> -> Pos + 29;
		<< _:Pos/bytes, _:30/bytes, Char:8, _/bytes >> -> Pos + 30;
		<< _:Pos/bytes, _:31/bytes, Char:8, _/bytes >> -> Pos + 31;
		<< _:Pos/bytes, _:32/bytes, _/bytes >> -> find_8( Buffer, Char, Pos + 32 );
		_ -> not_found
	end.


Страшно? :) Здесь я руками раскрутил цикл, для того, чтобы дать возможность компилятору сгенерировать длинный кусок native кода, оптимально скомпилировав паттерн-матчинг. В принципе, 32 строки - это перебор. Вполне адекватно работает уже на 8 строках. Но мне 10% производительности не лишние. Вообще - по хорошему эта функция должна быть реализована как BIF.

Теперь сделаем функцию, которая разделяет наш бинарис на два, по первому вхождению заданного символа. Вот так:

%% split_char( binary(), byte() ) -> { binary(), binary() } | not_found
split_char( Buffer, Char ) ->
	case find_8( Buffer, Char, 0 ) of
		not_found -> not_found;
		Pos ->
			<< Before:Pos/bytes, _:8, After/bytes >> = Buffer,
			{ Before, After }
	end.


Разумеется, никакого копирования данных опять не происходит. :) Все тихо и мирно :).

Ну вот. Теперь мы готовы к тому, чтобы сделать наш get_line. Начинается самое интересное. Hardcore erlang.

%% file_reader( File, Len ) -> Handle
%% Handle = { NextF, binary() } | eof
%% NextF = fun() -> Handle
file_reader( File, Len ) ->	file_reader( File, Len, << >> ).
file_reader( File, LenI, BufferB ) ->
	NextF = fun() ->
		case file:read( File, LenI ) of
			{ ok, DataB } -> file_reader( File, LenI, DataB );
			eof -> eof
		end
	end,
	{ NextF, BufferB }.


Вот так. Эта штука возвращает "итератор". Итератор на блоки данных, мы ведь блоками данные читать собрались. А именно - это тупл, второй элемент которого содержит текущий прочитанный блок данных, а первый элемент - это функция без аргументов, которая продвинет итератор вперед, и вернет следующий. Нафиг нам не нужно никакое гребанное ООП, чтоб писать такие штуки, оно только под руками путается да думать мешает.

Вот. Теперь осталость написать сам get_line.
get_line( { NextF, BufferB } ) ->
	case split_char( BufferB, 10 ) of
		{ LineB, RestB } -> { { NextF, RestB }, LineB };
		not_found ->
			case NextF() of
				eof -> { eof, BufferB };
				Handl_1 ->
					{ Handl_2, LineB } = get_line( Handl_1 ),
					{ Handl_2, << BufferB/bytes, LineB/bytes >> }
			end
	end.


Собственно, все. Обратите внимание, забавный момент, у меня это само собой получилось. get_line совершенно ничего не знает о файлах. Он знает, что надо дернуть функцию, и магически появится следующий блок. Поэтому, ее можно использовать с произвольным источником данных. Кто-то еще думает, что повторное использование и полиморфный код - прерогатива ООП, и для этого необходимы классы, и куча дизайн-паттернов? :)

get_line( iterator() ) -> { iterator(), line() }

теперь проверим, с какой скоростью работает ввод-вывод в Эрланге на самом деле.

tf( Name, Len ) ->
	{ ok, Fl } = file:open( Name, [ read, raw, binary ] ),
	tf_loop( file_reader( Fl, Len ) ),
	file:close( Fl ). 
%% where
tf_loop( eof ) -> done;
tf_loop( Hnd ) ->
	{ Hnd_2, _ } = get_line( Hnd ),
	tf_loop( Hnd_2 ).


Len - это длина буфера. Ее мы будем варьировать. Вот результаты тестирования. Чтение файла длиной 821 мегабайт. Компьютер - iMac G5 1,9 Gz. Компилируем с флагом native. Время - в секундах.

Buffer size (bytes)	256	1024	4096	8192	16384
default		91,3	53,6	44,1	42,1	41,7
read_ahead		60,6	44,9	42,1	41,2	40,9
async			265,2	102,1	59,2	46,9	44,6
async, read_ahead	59,7	45,7	42,6	41,7	40,3


default - без async threads и read ahead режимов. Далее - включаем эти два режима по очереди и одновременно. Делаем выводы.

Скорость чтения достигает 20 мегабайт в секунду. То есть, в Эрланге вполне адекватный ввод.-вывод. Впрочем, будь там оптимизированный BIF для поиска по binaries - было бы еще лучше, я думаю. Надо завести proposal.

В режиме async threads лучше всегда применять read ahead. Режим рантайма async threads - позволяет накладывать вычисления на ожидание ввода-вывода, это необходимая штука для приложений выполняющих активный ввод-вывод.

Тем более, что при включенном read ahead производительность не зависит от того, async или не async у нас threads.

Далее. Меньше килобайта размер буфера делать не надо ни при каких обстоятельствах, это понятно. Оптимальный размер окна в большинстве случаев - 4К.

Собственно, все. Смотрите на таблицу, делайте выводы.


Comments:


Serguey Zefirov
thesz at 2008-01-06 06:02 (UTC) (Link)
http://www.haskell.org/ghc/docs/latest/html/libraries/bytestring/Data-ByteString.html

ByteString - то же самое для Хаскеля.

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

Ну, и статья про этот кусок библиотеки побольше твоей будет. ;)
Gaperton
gaperton at 2008-01-06 10:16 (UTC) (Link)
Во первых - ByteString - это бледное подобие binaries, сделанное ручками для Хаскеля. Это в Эрланге все переложено на компилятор.

Во-вторых, моя заметка вообще не об этом. А о том, как делать быстрый line-oriented IO в Эрланге. Это не "то же самое", здесь вообще мало общего с твоей ссылкой.

В третьих - меня очень радует, что статья про этот кусок библиотеки будет побольше моей заметки :). Это живой пример важной истине - что в мире есть вообще очень много разных текстов, которые превышают мою заметку по размеру. :)
Serguey Zefirov
thesz at 2008-01-06 12:42 (UTC) (Link)
>Во первых - ByteString - это бледное подобие binaries, сделанное ручками для Хаскеля. Это в Эрланге все переложено на компилятор.

Что говорит о чем? Правильно, что в других языках возложено на компилятор, в Хаскеле может быть сделано программистом. Причем унифицированным и проверяемым компилятором способом.

И байтстринги - это не бинарис.

>Во-вторых, моя заметка вообще не об этом. А о том, как делать быстрый line-oriented IO в Эрланге. Это не "то же самое", здесь вообще мало общего с твоей ссылкой.

Байтстринги практически про это же самое. Про быстрый ввод-вывод и экономию памяти.

Если читать из файла не байтстринги, то получается список наподобие Эрланговского - по 8+ байт на символ.

Так что, считаю, вы в одной нише находитесь. ;)
Andy Melnikov
nponeccop at 2008-01-06 15:06 (UTC) (Link)

Буга-га!

У меня вызвали смех фразы "Скорость чтения достигает 20 мегабайт в секунду." и "Оптимальный размер окна в большинстве случаев - 4К.".

Во-первых, Вы же не для MFM пишете. Современные винты обеспечивают 80 мб/сек последовательное чтение. Даже если ваш винт очень древний, сомневаюсь что 20 мб/сек это сколь-нибудь близко к его предельной скорости. В любом случае нужно выложить значения физической производительности винта, чтобы было понятно, насколько же это эффективно. То есть, тесты без указания теоретического предела - нерепрезентативны.

Во-вторых, Вы же не для бейсика под дос пишете. Нужно читать несколько файлов параллельно. При этом выяснится существование у винчестеров очень большой seek latency, для компенсации которой понадобятся буферы значительно толще 4кб, и ваша рекомендация потеряет всякий смысл. То есть, однопоточные тесты - нерепрезентативны.

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

В-четвертых, большие буферы, необходимые для компенсации seek latency, скорее всего компенсируют и ipc latency, из-за которой обычные файлы с суррогатным процессом работают медленно. То есть, возможно, чтение мегабайтных блоков из "обычного" файла будет ненамного медленнее чтения из "raw" файла, и можно будет пользоваться преимуществами распределенности. То есть, тесты без сравнения "обычных" и "raw"-файлов - нерепрезентативны.
Gaperton
gaperton at 2008-01-06 17:53 (UTC) (Link)

Re: Буга-га!

> У меня вызвали смех фразы "Скорость чтения достигает 20 мегабайт в секунду." и "Оптимальный размер окна в большинстве случаев - 4К.".

Не вижу ничего смешного в обеих фразах, или "смех без причины - признак дурачины". Первая характеризует скорость чтения, показанную тестом. Чего смешного? С вами все в порядке вообще?

Поясняю смысл второй фразы - для особо одаренных, не вкуривших, о чем это я. В ней речь идет о размере ВНЕШНЕГО буфера. Кроме внешнего буфера, есть еще ВНУТРЕННИЙ read_ahead буфер внутри драйвера, внешний буфер надо держать как можно меньшего размера. Он служит для исключительно для уменьшения количества обращений к драйверу file, не более того, что кстати, хорошо видно на результатах теста.

> Современные винты обеспечивают 80 мб/сек последовательное чтение. Даже если ваш винт очень древний, сомневаюсь что 20 мб/сек это сколь-нибудь близко к его предельной скорости. В любом случае нужно выложить значения физической производительности винта, чтобы было понятно, насколько же это эффективно. То есть, тесты без указания теоретического предела - нерепрезентативны.

Меня совершенно не интересует абстрактная эффективность, и никаких тестов я не писал. Скорость 20 мег/сек - более чем адекватна для многих применений. Надо еще успеть обработать данные, считанные в таком темпе. Да, аналогичная С-шная программа показывает скорость чтения 60 мег/сек, если вам интересно.

> Во-вторых, Вы же не для бейсика под дос пишете. Нужно читать несколько файлов параллельно. При этом выяснится существование у винчестеров очень большой seek latency, для компенсации которой понадобятся буферы значительно толще 4кб, и ваша рекомендация потеряет всякий смысл. То есть, однопоточные тесты - нерепрезентативны.

Вам нужно замерять параллельно - замеряйте. Мне замерять скорость рандом-рида моего диска совершенно не интересно. Мне интересна производительность библиотеки в пиковом режиме.

> В-третьих, чистый файловый ввод-вывод в современных ОС и на современном оборудовании (я так говорю потому что слабо представляю что в G5 стоит) занимает единицы процентов процессорного времени.

Для справки насчет единиц процентов процессорного времени. Аналогичная программа на С ОТЖИРАЕТ 50% CPU. При этом, в данной программе нет НИКАКОЙ обработки прочитанных строк, голый цикл.

Почему так. Потому, видимо, что вы плохо себе представляете, что это такое - читать данные со скоростью 60 мег/сек. У PowerPC G5 тактовая частота 2Гигагерца, и на каждый прочитанный байт из 60 мегабайт в секунду приходится 33 такта, что соответствует в среднем 100 выполняемым инструкциям на каждый считанный байт. Эрланг неспособен сделать что-либо осмысленное с данными, поступающими в таком темпе, да и в случае С++ это будет сделать не просто - только простейшую обработку успеешь сделать, и все.

Да, проц G5 всего лишь немного слабее Core 2 Duo - в основном благодаря более высоколатентной подсистеме памяти и примерно вчетверо меньшему размеру кэша L2, что в данной задаче не играет совершенно. Примерно такие ядра, как G5, стоят в современных IBM-овских мэйнфреймах.

Далее поскипал, звиняйте. Свежее садомазохистическое предложение тестировать обычный эрланговский io на блоках размером в мегабайт проигнорировал. :) Хотя идея меня улыбнула :). Пожалуй, будет лучше, если этот эпохальный эксперимент проведете вы сами, уважаемый коллега :).
Andy Melnikov
nponeccop at 2008-01-06 18:21 (UTC) (Link)

Всё ли в порядке ли со мной

> Чего смешного? С вами все в порядке вообще?

Нет, со мной не всё в порядке - я сишник. Поэтому привык к сишным порядкам скорости. Соответственно, гордость значением в 20 мб-сек вызывает у меня умиление точно так же, как у thesz вызывает умиление уровень на котором работают джависты. Просто интересно узнавать, что происходит в других областях, и неожиданные результаты вызывают смех.
Gaperton
gaperton at 2008-01-06 19:35 (UTC) (Link)

Re: Всё ли в порядке ли со мной

1) Erlang - не конкурент С. Это динамически типизированный язык. Если сравнивать его производительность на микротестах - он наиболее близок к Питону, Руби, Схеме, Лиспу. Глупо подходить к нему с мерками С.

2) "Скорость чтения достигает 20 мегабайт в секунду. То есть, в Эрланге вполне адекватный ввод.-вывод. Впрочем, будь там оптимизированный BIF для поиска по binaries - было бы еще лучше, я думаю. Надо завести proposal."

Я вижу, что я сообщил рельзультат теста, и заключил, что скорость вполне адекватна. Где вы тут углядели "гордость результатом" - покажите? Впрочем, каждый видит то, что хочет видеть. Я, например, не вижу ничего кроме смеха без причины.
Andy Melnikov
nponeccop at 2008-01-06 18:29 (UTC) (Link)

Размер буфера и кол-во обращений

> Поясняю смысл второй фразы - для особо одаренных, не вкуривших, о чем это я. В ней речь идет о размере ВНЕШНЕГО буфера. Кроме внешнего буфера, есть еще ВНУТРЕННИЙ read_ahead буфер внутри драйвера, внешний буфер надо держать как можно меньшего размера. Он служит для исключительно для уменьшения количества обращений к драйверу file, не более того, что кстати, хорошо видно на результатах теста.

Смысл любого файлового буфера - в уменьшении обращений к нижележащим слоям. Я не понимаю к чему это?

В Windows буферы (находящиеся на различных уровнях) служат тем же целям. Но увеличение буфера в приложении с 4 кб до нескольких мегабайт приносит ощутимый эффект. Я не исключаю, что Вы не заметили особого эффекта в своих тестах после 4 кб только в силу того что не измеряли другие параметры, о которых я сказал в посте.
Andy Melnikov
nponeccop at 2008-01-06 18:38 (UTC) (Link)

Писали ли Вы тест? (не по сути)

Вы же сами называете то, что писали тестом:

> Первая характеризует скорость чтения, показанную тестом.

Но тут же себе противоречите:

> Меня совершенно не интересует абстрактная эффективность, и никаких тестов я не писал.

Нехорошо, батенька, возражать лишь из желания возразить.
Andy Melnikov
nponeccop at 2008-01-06 18:45 (UTC) (Link)

Голый цикл и проценты времени

> Для справки насчет единиц процентов процессорного времени. Аналогичная программа на С ОТЖИРАЕТ 50% CPU. При этом, в данной программе нет НИКАКОЙ обработки прочитанных строк, голый цикл.

Если Вы заметили, я был в своем посте политкорректен и не упоминал языка Си. Дело в том, что реализация чтения строк в стандартной библиотеке языка Си очень кривая, пригодная только для чтения конфиг-файлов. И если надо читать строки быстро - надо писать собственную реализацию. У меня такая реализация построчного чтения текстовых файлов есть, могу Вам её дать, и она обеспечит Вам задекларированные мной проценты на Win32 и Win64 (к сожалению, используются ф-циии WinAPI, что делает её непереносимой).
Andy Melnikov
nponeccop at 2008-01-06 18:57 (UTC) (Link)

Aбстрактная эффективность

> Меня совершенно не интересует абстрактная эффективность

Начнем с эффективности. Вы же пишете:

> Мы сделаем самое быстрое в мире на момент публикации построчное чтение текстовых файлов для Эрланга.

> Действительно, файловый ввод-вывод в Эрланге имеет репутацию очень медленного. Просто ужасно медленного.

Значит, эффективность Вас интересует.

Теперь насчет абстрактности. Целью моих уточнений и было как раз приблизить ваш тест к реальной ситуации, и объяснил, что я вижу несколько причин, по которым производительность Эрланговского родного ио будет не такой уж плохой в реальном, а не абстрактном, окружении. Так что если Вас действительно интересует не абстрактная, а реальная производительность, Вы должны согласиться с моими доводами и провести тесты в реальном окружении.

Только после таких всесторонних тестов Вы сможете утверждать о "самом быстром в мире на момент публикации построчном чтении текстовых файлов для Эрланга", а сейчас это так, бессмыслица и вещь в себе. Вы даже "родной" производительности не привели, не с чем ваши супероптимальные 20 мб-сек сравнить. Если привели, ткните носом, пожалуйста.
Andy Melnikov
nponeccop at 2008-01-06 19:28 (UTC) (Link)

60 мег/сек

> вы плохо себе представляете, что это такое - читать данные со скоростью 60 мег/сек.

Нет, я как раз очень хорошо представляю чтение данных со скоростью 60 мег/сек. Моя суперпрограмма обрабатывает данные со скоростью 0.7 мбайт/сек/ядро, что на 80 ядрах (10 блейдов Dual Xeon Quad) выходит в 56 мб/сек. Еще есть мелкие, но нужные каждый день программы вроде grep, которые тоже вполне способны потребить 60 мбайт/сек.

И если моя программа будет читать данные со скоростью 60 мбайт/сек, времени не хватит для того чтобы успеть отдать 56 мб/сек в сеть и принять уже больше 60 мб/сек "обратки". Поэтому лично мне нужно уметь читать текстовые файлы со скоростью много большей 60 мб/сек.
Andy Melnikov
nponeccop at 2008-01-06 19:34 (UTC) (Link)

Рандом рида

> Вам нужно замерять параллельно - замеряйте. Мне замерять скорость рандом-рида моего диска совершенно не интересно. Мне интересна производительность библиотеки в пиковом режиме.

Так в том-то и дело, что "производительность библиотеки в пиковом режиме" - это сферический конь в вакууме. Вы же даете на основании своих измерений практические рекомендации разработчикам: "Оптимальный размер окна в большинстве случаев - 4К.". Я только уточнил, что не в большинстве случаев, а в случае сферической программы в вакууме, для того чтобы несколько ПОСЛЕДОВАТЕЛЬНЫХ чтений не превращались в одно чтение В СЛУЧАЙНОМ ПОРЯДКЕ, нужно использовать очень большие буферы. Более того, та же самая ситуация возникнет, если параллельно с чтением выполняется запись или другое произвольное чтение, что встречается сплошь и рядом.
Andy Melnikov
nponeccop at 2008-01-06 19:36 (UTC) (Link)

Мазохизм

> Свежее садомазохистическое предложение тестировать обычный эрланговский io на блоках размером в мегабайт проигнорировал. :) Хотя идея меня улыбнула :). Пожалуй, будет лучше, если этот эпохальный эксперимент проведете вы сами, уважаемый коллега :).

Я же не Ваш коллега. И Вы не указали, насколько родной ио медленный. Мне как сишнику чтение мегабайтных блоков вполне привычно, поясните в чем дело. Он что, 50 кбайт/сек читает?
Dmitrii 'Mamut' Dimandt
dmitriid at 2008-01-07 11:23 (UTC) (Link)
20 МБ/сек - это круто. Но меня больше впечатлил итератор. Это еще осыслить надо... Спасибо
ЕдинственныйКтоСтоитТам
stebanoid at 2008-01-07 13:10 (UTC) (Link)

мои 5 копеек

Ты создал интерфейс
find_8( Buffer, Char ) -> find_8( Buffer, Char, 0 ).
но ни разу им не воспользовался.

А итератор забавный. Я бы, наверное, что нибудь в том же духе и сделал бы, но на 2 страницы кода минимум.. :)
(Anonymous) at 2008-01-07 14:31 (UTC) (Link)
Выходит binaries в эрланге можно использовать как массивы?! Что-то типа
elem(Array, Index) ->
<
[Error: Irreparable invalid markup ('<_:(index>') in entry. Owner must fix manually. Raw contents below.]

Выходит binaries в эрланге можно использовать как массивы?! Что-то типа
elem(Array, Index) ->
<<_:(Index - 1)/bytes, Elem:8, _/bytes>> = Array.

До прочтения статьи всё искал как же с большими данными работать. Спасибо за подсказку.
Да, если будут BIF-ы для работы с бинарным типом (поиск, доступ по индексу) будет вообще счастье.
Dmitrii 'Mamut' Dimandt
dmitriid at 2008-01-07 16:24 (UTC) (Link)
http://forum.trapexit.org/mailinglists/viewtopic.php?t=11940 (как раз про fast text i/o, я туда запостил этот пост :) )

там есть вот такое:

I found that the following function is about 10% faster then the unrolled function when using BEAM R12B:

find_8(Buffer, Char, Pos) ->
  case Buffer of
    << Char, _/bits >> -> Pos;
    << _, Rest/bits >> ->
      find_8(Rest, Char, Pos+1);
    _ ->
      not_found
  end.

It might depend a little on the input though and when both functions were native compiled there was no major difference between them.
Gaperton
gaperton at 2008-01-07 23:04 (UTC) (Link)
Пер Берквист неправ. Моя unrolled функция вдвое быстрее, чем запись в одну строку, когда она скомпилирована с флагом native. Я же не просто так подбирал количество строк в ней - я мерял производительность, и провел необходимые тесты.

А вот без native существенной разницы быть не должно. Потому, что unrolling помогает только машинному коду для superpiplined процессоров. Виртуальная машина, разумеется, не superpiplined, так что толку от unrolling-а циклов будет немного.

Так что единственное упрощение, которое можно ввести в расчете на R12, состоит вот в чем:

find_8( Buffer, Char, Pos ) ->
case Buffer of
<< Char:8, _/bytes >> -> Pos;
<< _:1/bytes, Char:8, _/bytes >> -> Pos + 1;
<< _:2/bytes, Char:8, _/bytes >> -> Pos + 2;
...
<< _:32/bytes, Rest/bytes >> -> find_8( Rest, Char, Pos + 32 );
_ -> not_found
end.

И все.
Gaperton
gaperton at 2008-01-07 23:45 (UTC) (Link)

Упс...

Это был не Берквист, а Густавсон. Ну, тоже хороший дядька :)
Dmitrii 'Mamut' Dimandt
dmitriid at 2008-01-08 12:16 (UTC) (Link)

Re: Упс...

Там продолжение :)

Anyway, the original, unrolled find_8 version together with this code (which avoids splitting the buffer unnecessarily) is a tiny bit faster on my machine:


%% file_reader( File, Len ) -> Handle
%% Handle = { NextF, binary(), Pos } | eof
%% NextF = fun() -> Handle
file_reader( File, Len ) ->    file_reader( File, Len, << >> ).
file_reader( File, LenI, BufferB ) ->
   NextF = fun() ->
       case file:read( File, LenI ) of
           { ok, DataB } -> file_reader( File, LenI, DataB );
           eof -> eof
       end
   end,
   { NextF, BufferB, 0 }.

get_line( { NextF, BufferB, Pos } ) ->
    case find_8(BufferB, 10, Pos) of
	not_found ->
	    case BufferB of
		<< _:Pos/bytes, RestB/bytes >> -> 
		    case NextF() of
			eof -> {eof, RestB};
			Handl_1 ->
			    { Handl_2, LineB } = get_line( Handl_1 ),
			    { Handl_2, << RestB/bytes, LineB/bytes >> }
		    end
	    end;
	P -> LineSize = P - Pos, 
	     case BufferB of
		 << _:Pos/bytes, LineB:LineSize/bytes, _/bytes >> ->
		     {{ NextF, BufferB, P + 1}, LineB}
	     end
    end.


(This code is only lightly tested, there could be a fatal bug).
Dmitrii 'Mamut' Dimandt
dmitriid at 2008-12-14 21:02 (UTC) (Link)
Я тут это дело в вики закинул: http://wk.rsdn.ru/БыстрыйIOвErlang.ashx

Надеюсь, можно :)
Макс Лапшин
levgem at 2012-01-09 19:18 (UTC) (Link)
с тех пор что-то поменялось?
Gaperton
gaperton at 2012-01-10 13:27 (UTC) (Link)
Конечно.

Во-первых, появился модуль binary, а в нем - функция split.

Эта функция должна работать существенно быстрее, чем мой split_char на Эрланге. Учитывая, что основная просадка моего кода именно в split_char, применение binary:split должно сильно поправить скорость чтения.

Во-вторых, поменялась функция io:get_line. Она теперь умеет возвращать unicode_binary(). Я не знаю, насколько это сказалось на ее производительности, надо измерять.

Ну и в третьих, и это, вероятно, главное - в модуле file появилась функция read_line. У которой нет никаких причин работать медленно, и по идее она должна сделать весь этот мой код не нужным :).

Все перечисленное касается только построчного чтения. Блочное чтение и так было быстрым.
Макс Лапшин
levgem at 2012-01-10 14:19 (UTC) (Link)
Вот удивительно. Но для чтения CSV мне пришлось написать NIF, это оказалось во много сотен раз быстрее.
Previous Entry  Next Entry