Rambler's Top100 Service калинин.ru / программирование / unix /  << 20.05.01 >>

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'а.
/comment/books/03_12_00.shtml
   Uresh Vahalia, Unix Internals (книга на английском языке).
/comment/books/31_10_00.shtml
   А. Робачевский, Операционная система Unix.
  Рядом в разделе:
Потоки (03.07.01)
   Хочется наконец-то выполнить данное мною еще полгода назад обещание и рассказать о том, что такое потоки в Unix и каких типов...   >>>>
  Рядом по дате:
pregrad.net, заказ товаров из интернет-магазинов Европы и США (08.06.01)
   Некоторое время назад передо мной встала достаточно серьезная проблема --- явная нехватка литературы по некоторым, живо интересующим меня вопросам. Оказалось, что...   >>>>
www.researchindex.com, The NECI Scientific Literature Digital Library (07.05.01)
   К сожалению, приходится признать, что для русскоязычного специалиста ощущается достаточно большая нехватка информации. Причем, если по достаточно общеизвестным, популярным или попросту...   >>>>
  Содержание:
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
   C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Просто так
Студенческое
Туризм
  Байки
Фотографии
Комментарии
   Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Благодарности
Форум
Хронология
 
  В этом разделе:
Потоки (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-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Студенческое
Просто так
Благодарности
Форум
Хронология
© 2000-2008, Andrey L. Kalinin
mailto:andrey@kalinin.ru
Rambler's Top100