Как улучшить параллелизм пропускной способности ввода-вывода SSD в Linux

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

К сожалению, это не работает на моей машине. Запуск двух копий программы лишь немного (если вообще) быстрее, чем запуск одной копии (см. Ниже), и менее 20% от того, на что способен мой привод. На машине с Ubuntu с идентичным оборудованием ситуация немного лучше. Я получаю линейное масштабирование для 3-4 ядер, но при этом до сих пор достигаю 50% емкости моего SSD-накопителя.

Какие препятствия мешают линейному масштабированию пропускной способности ввода-вывода при увеличении количества ядер, и что можно сделать для улучшения параллелизма ввода-вывода на стороне программного обеспечения / ОС?

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

Подробности:

Вот несколько строк iostat -x 1 выход:

Копирование файла в / dev / null с помощью dd:

Device:         rrqm/s   wrqm/s     r/s     w/s     rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               0.00     0.00  883.00    0.00 113024.00     0.00   256.00     1.80    2.04    2.04    0.00   1.13 100.00

Запуск моей программы:

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               1.00     1.00  141.00    2.00 18176.00    12.00   254.38     0.17    1.08    0.71   27.00   0.96  13.70

Запуск двух экземпляров моей программы одновременно, чтение разных файлов:

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda              11.00     0.00  139.00    0.00 19200.00     0.00   276.26     1.16    8.16    8.16    0.00   6.96  96.70

Это чуть лучше! Добавление большего количества ядер не увеличивает пропускную способность, фактически оно начинает ухудшаться и становится менее согласованным.

Вот один экземпляр моей программы и один экземпляр dd:

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               9.00     0.00  468.00    0.00 61056.00     0.00   260.92     2.07    4.37    4.37    0.00   2.14 100.00

Вот мой код:

#include <string>

#include <boost/filesystem/path.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/fstream.hpp>

typedef boost::filesystem::path path;
typedef boost::filesystem::ifstream ifstream;

int main(int argc, char ** argv) {
path p{std::string(argv[1])};
ifstream f(p);
std::string line;
std::vector<boost::iterator_range<std::string::iterator>> fields;

for (getline(f,line); !f.eof(); getline(f,line)) {
boost::split (fields, line, boost::is_any_of (","));
}
f.close();
return 0;
}

Вот как я это скомпилировал:

g++ -std=c++14 -lboost_filesystem -o gah.o -c gah.cxx
g++ -std=c++14 -lboost_filesystem -lboost_system -lboost_iostreams -o gah gah.o

Изменить: еще больше деталей

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

Мой процесс, похоже, связан с процессором; переключение на mmap или изменение размера буфера через pubsetbuf не оказывают заметного влияния на записываемую пропускную способность.

С другой стороны, масштабирование связано с IO. Если я перенесу все файлы в кэш памяти перед запуском моей программы, пропускная способность (теперь измеряется временем выполнения с iostat не видно) масштабируется линейно с количеством ядер.

Что я действительно пытаюсь понять, так это то, что, когда я читаю с диска, используя несколько процессов последовательного чтения, почему пропускная способность не масштабируется линейно с количеством процессов до уровня, близкого к максимальной скорости чтения диска? Зачем мне попадать в границы ввода / вывода без насыщения пропускной способности, и как, когда я это делаю, зависит от стека ОС / программного обеспечения, над которым я работаю?

4

Решение

Вы не сравниваете подобные вещи.

Ты сравниваешь

Copying a file to /dev/dull with dd:

(Я предполагаю, что вы имели в виду /dev/null…)

с

int main(int argc, char ** argv) {
path p{std::string(argv[1])};
ifstream f(p);
std::string line;
std::vector<boost::iterator_range<std::string::iterator>> fields;

for (getline(f,line); !f.eof(); getline(f,line)) {
boost::split (fields, line, boost::is_any_of (","));
}
f.close();
return 0;
}

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

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

2

Другие решения

Я считаю, что здесь было как минимум три проблемы:

1) Мои чтения происходили слишком регулярно.

Файл, из которого я читал, имел строки предсказуемой длины с предсказуемо расположенными разделителями. Случайно вводя задержку в 1 микросекунду один раз из тысячи, я смог увеличить пропускную способность между несколькими ядрами примерно до 45 МБ / с.

2) Моя реализация pubsetbuf фактически не установила размер буфера.

Стандарт определяет, что pubsetbuf отключает буферизацию, когда указан нулевой размер буфера, как описано в эта ссылка (спасибо, Эндрю Хенле); все остальное поведение определяется реализацией. Видимо моя реализация использовала размер буфера 8191 (проверено strace), независимо от того, какое значение я установил.

Будучи слишком ленивым, чтобы реализовать мою собственную потоковую буферизацию для целей тестирования, я переписал код для чтения 1000 строк в вектор, затем попытался проанализировать их во втором цикле, затем повторил всю процедуру до конца файла (случайных задержек не было) ). Это позволило мне масштабироваться примерно до 50 МБ / с.

3) Мой планировщик ввода-вывода и настройки не подходили для моего привода и приложения.

По-видимому, Arch Linux по умолчанию использует cfq io-планировщик для моего SSD-накопителя с параметрами, подходящими для HDD-накопителей. настройка slice_sync до 0, как описано Вот (см. ответ Микко Ранталайнена и связанную статью), или переключение на noop планировщик, как описано Вот, исходный код получает максимальную пропускную способность около 60 МБ / с при использовании четырех ядер. Эта ссылка было также полезно.

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

1

По вопросам рекламы [email protected]