Программа, представленная ниже, читает несколько строк из файла и анализирует их. Это может быть быстрее. С другой стороны, если мне нужно обработать несколько ядер и несколько файлов, это не должно иметь большого значения; Я могу просто выполнять задания параллельно.
К сожалению, это не работает на моей машине. Запуск двух копий программы лишь немного (если вообще) быстрее, чем запуск одной копии (см. Ниже), и менее 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
не видно) масштабируется линейно с количеством ядер.
Что я действительно пытаюсь понять, так это то, что, когда я читаю с диска, используя несколько процессов последовательного чтения, почему пропускная способность не масштабируется линейно с количеством процессов до уровня, близкого к максимальной скорости чтения диска? Зачем мне попадать в границы ввода / вывода без насыщения пропускной способности, и как, когда я это делаю, зависит от стека ОС / программного обеспечения, над которым я работаю?
Вы не сравниваете подобные вещи.
Ты сравниваешь
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
Команда, в которой вы сравниваете свой код, чтобы никогда не тратить время на выполнение других операций, кроме чтения данных — не нужно читать, затем обрабатывать, затем читать, затем обрабатывать …
Я считаю, что здесь было как минимум три проблемы:
1) Мои чтения происходили слишком регулярно.
Файл, из которого я читал, имел строки предсказуемой длины с предсказуемо расположенными разделителями. Случайно вводя задержку в 1 микросекунду один раз из тысячи, я смог увеличить пропускную способность между несколькими ядрами примерно до 45 МБ / с.
2) Моя реализация pubsetbuf фактически не установила размер буфера.
Стандарт определяет, что pubsetbuf отключает буферизацию, когда указан нулевой размер буфера, как описано в эта ссылка (спасибо, Эндрю Хенле); все остальное поведение определяется реализацией. Видимо моя реализация использовала размер буфера 8191 (проверено strace
), независимо от того, какое значение я установил.
Будучи слишком ленивым, чтобы реализовать мою собственную потоковую буферизацию для целей тестирования, я переписал код для чтения 1000 строк в вектор, затем попытался проанализировать их во втором цикле, затем повторил всю процедуру до конца файла (случайных задержек не было) ). Это позволило мне масштабироваться примерно до 50 МБ / с.
3) Мой планировщик ввода-вывода и настройки не подходили для моего привода и приложения.
По-видимому, Arch Linux по умолчанию использует cfq
io-планировщик для моего SSD-накопителя с параметрами, подходящими для HDD-накопителей. настройка slice_sync
до 0, как описано Вот (см. ответ Микко Ранталайнена и связанную статью), или переключение на noop
планировщик, как описано Вот, исходный код получает максимальную пропускную способность около 60 МБ / с при использовании четырех ядер. Эта ссылка было также полезно.
С noop
планирование, масштабирование кажется почти линейным, вплоть до четырех физических ядер моей машины (у меня восемь с гиперпоточностью).