write() или writev()?
Меня давно интересовал вопрос, что и когда лучше использовать,
вызов write() или writev()? Именно ответом на него я начну новую
рубрику на моем хоумпейджере.
Для людей, не знакомых с системными вызовами в Unix'е, постараюсь
вкратце объяснить что это такое. Прототип write()
выглядит следующим образом:
ssize_t write(int d, const void *buf, size_t nbytes);
Эта функция позволяет записать nbytes байт из буфера
buf в файл или сокет, определяемый дескриптором
d . Когда вы в своей программе используете этот вызов,
то это на самом деле функция-заглушка, обращающаяся потом к
реальному вызову ядра. При этом, для каждого типа дескриптора
(файл на диске с файловой системой ufs, файл в сетевой файловой
системе nfs, сокет) используется своя версия вызова,
предоставляемая интерфейсом этого типа идентификаторов.
Заметьте -- ядро Unix'а написано на C, но при этом является вполне
объектно-ориентированным.
При этом, когда пользовательская программа передает указатель на
буфер, то он на самом деле внутренний для процесса, и для того,
чтобы ядро могло бы получить доступ к данным процесса, приходится
делать преобразование адреса. После того, как преобразование
сделано, данные из буфера копируются во внутренние буфера ядра
и происходит вызов внутренних подпрограмм для соответствующего
типа дескриптора.
Абзац выше --- попытка на пальцах объяснить достаточно сложные
вещи и верен лишь в первом приближении (в том смысле, что можно еще
много чего уточнить), но его хватит. То есть,
надо понять, что, во-первых, при вызове write()
выполняется переход из контекста процесса в контекст ядра и,
во-вторых, происходит копирование данных внутрь памяти ядра.
Вызов writev() несколько отличается по своему
прототипу:
ssize_t writev(int d, const struct iovec *iov, int iovcnt);
Где структура iovec определяется следуюшим образом:
struct iovec {
char *iov_base;
size_t iov_len;
};
Через массив iov передаются адреса и размеры буферов,
которые должны быть записаны в дескриптор d . Этот
вызов существует потому, что очень часто приходится сбрасывать
несколько структур одновременно в файл, и если использовать
write() и стремиться к атомарности операций (что
вполне естественно: запись большого количества данных на диск
гораздо быстрее, чем многократная запись маленьких кусочков, это
связано с тем, что в этом случае время поиска места на диске будет
значительно более существенным, чем время непосредственной
записи), тогда придется делать большой буфер, куда копировать
(например, через memcpy() ) содержимое маленьких записей...
Это означает, что при вызове write() и предыдущим
сбором данных в один буфер, сначала будет выполнено много
копирований внутри процесса (memcpy() не производит
переключения контекста процесса, понятно что это не надо при
копировании данных внутри адресного пространства процесса), а
потом будет выполнено одно копирование из адресного пространства
процесса в адресное пространство ядра.
Вызов writev() позволяет сразу же передать ядру много
указателей и позволяет убрать лишнее копирование данных внутри
процесса, собрав их сразу же в буфер внутри ядра.
Вроде бы, так как количество копирований данных становится меньше,
вызов writev() должен работать быстрее, чем
write в случае нескольких буферов.
На самом деле, это не так. Все дело в том, что при использовании
write() выполняется только одно копирование из
пространства процесса в пространство ядра, а при использовании
writev() --- несколько. При этом, во-первых, на
каждое такое копирование уходит время на вычисление адреса,
понятного ядру, по адресу процесса, и, во-вторых, на sdram-памяти
операция одного последовательного копирования большого объема
данных быстрее, чем много операций копирования маленьких кусочков
(при равной сумме объемов).
Соответственно, вопрос заключается в том, когда лучше использовать
write() , а когда -- writev() и вообще,
справедливы ли рассуждения выше.
Для этого была написана очень простая программа, которая
записывает несколько раз на диск "что-то" через
writev() и write() :
#include <sys/types.h>
#include <sys/uio.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <fcntl.h>
#include <syslog.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define NUM_ELEMS 1000
#define ELEM_SIZE 1000
#define WRITES_COUNT 100
iovec iov[NUM_ELEMS];
char* data[NUM_ELEMS];
char buf[NUM_ELEMS*ELEM_SIZE];
void iov_write(int fd)
{
for(int i = 0; i < NUM_ELEMS; i++)
{
iov[i].iov_len = ELEM_SIZE;
iov[i].iov_base = data[i];
}
writev(fd, iov, NUM_ELEMS);
}
void buf_write(int fd)
{
for(int i = 0; i < NUM_ELEMS; i++)
memcpy(buf + i*ELEM_SIZE, data[i], ELEM_SIZE);
write(fd, buf, NUM_ELEMS*ELEM_SIZE);
}
int main()
{
for(int i = 0; i < NUM_ELEMS; i++)
data[i] = (char*)malloc(ELEM_SIZE);
int fd = open("test_write", O_RDWR | O_TRUNC | O_CREAT, 0644);
for(int i = 0; i < WRITES_COUNT; i++)
{
iov_write(fd);
buf_write(fd);
}
close(fd);
return 0;
}
На самом деле, я еще инициализировал data[i] , чтобы
он был заведомо ненулевым.
Эта программа компилировалась командой вида:
g++ -pg t_write.cpp
И получившийся профайл изучался на предмет количества времени,
затраченного в функциях buf_write() и
iov_write() . При этом суммировалось системное и
пользовательское время. Надо сказать, что параметры оптимизации,
конечно же, никак не влияют на количество затраченного времени
внутри этих функций (по понятным причинам --- большая часть
времени тратится на то, до чего оптимизатору не дотянуться).
Программа компилировалась с различными параметрами (которые
выделены вверху в define), некоторые результаты я приведу (для
NUM_ELEMS и WRITES_COUNT тех же, что и в программе, но разным
ELEM_SIZE).
ELEM_SIZE iov_write buf_write
1000 1.59ms 2.34ms
500 0.96 1.27
300 0.55 0.76
200 0.38 0.50
100 0.23 0.21
50 0.10 0.09
10 0.03 0.02
5 0.04 0.02
3 0.03 0.02
1 0.03 0.02
В общем-то, к единицам измерения надо относится так, что это время
на машине средней паршивости, с RAID-массивом и памятью годовалой
давности. Для более быстрых или более медленных машин результаты
будут иными, в частности для моей домашнего, уже очень старого
компьютера, iov_write() был заметно лучше уже при
размере ELEM_SIZE 100.
Это говорит о том, что использование writev()
оправданно тогда, когда есть большие массивы данных, и хотя
граница "большие-маленькие" размыта по скорости компьютра, можно утверждать
что, к примеру, для записи целых чисел использование
writev() совершенно неразумно, лучше собирать все в
один массив и его передавать к write() .
Еще хочется отметить, что при размере элемента 1,3 и 5 байтов
померять точно, сколько будет выполняться функция, нельзя, потому
что при этом сказывается выполнение других процессов (для более
точных измерений
надо увеличить количество записываемых элементов) и цифры в
таблице приведены усредненные для нескольких запусков. Для больших
размеров, время выполнения практически не изменялось от запуска к
запуску.
Резюме
Как видно из представленных чисел, вызов writev()
хорош тогда, когда в списке буферов есть такие, которые по размеру
больше хотя бы 200 байт, до того лучше использовать
write() . И хотя число-граница может колебаться в
зависимости от компьютера и операционной системы (для опытов
использовалась FreeBSD 4.3), точно надо использовать
write() для маленьких размеров буферов.
Версия для печати
| Ссылки по теме: |
 |
http://lib.ru/BACH/ |
|
Морис Дж. Бах, "Операционная система Unix". Уже старая книга, содержащая информацию о том, "как это было", но все равно интересная для тех, кто не знаком с устройством Unix'а.
|
|
 |
|
| Рядом в разделе: |
 |
Потоки (03.07.01) |
|
Хочется наконец-то выполнить данное мною еще полгода назад обещание и рассказать о том, что такое потоки в Unix и каких типов... >>>>
|
|
 |
|
| Содержание: |
 |
|
|
| В этом разделе: |
 |
Потоки (03.07.01) |
|
Хочется наконец-то выполнить данное мною еще полгода назад обещание и рассказать о том, что такое потоки в Unix и каких типов... >>>>
|
write() или writev()? (20.05.01) |
|
Меня давно интересовал вопрос, что и когда лучше использовать, вызов write() или writev()? Именно ответом на него я начну новую рубрику... >>>>
|
Содержание раздела полностью... |
|
Примерно в тоже время |
 |
pregrad.net, заказ товаров из интернет-магазинов Европы и США (08.06.01) |
|
Некоторое время назад передо мной встала достаточно серьезная проблема --- явная нехватка литературы по некоторым, живо интересующим меня вопросам. Оказалось, что... >>>>
|
www.researchindex.com, The NECI Scientific Literature Digital Library (07.05.01) |
|
К сожалению, приходится признать, что для русскоязычного специалиста ощущается достаточно большая нехватка информации. Причем, если по достаточно общеизвестным, популярным или попросту... >>>>
|
Хронология полностью... |
|
Содержание |
 |
Заглавная страница |
Мой блог |
Мое резюме |
Дайджест |
Программирование |
|
C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
|
TeX |
Туризм |
|
Байки
Фотографии
|
Комментарии |
|
Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
|
Студенческое |
Просто так |
Благодарности |
Форум |
Хронология |
|