Как заставить IOStream работать лучше?

Большинство пользователей C++, изучивших C, предпочитают использовать семейство функций printf/scanf, даже когда они пишут код на C++.

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

Взглянув на этот вопрос:

Как я могу ускорить построчное чтение файла

Кажется, что лучший ответ - использовать fscanf, а C++ ifstream постоянно в 2-3 раза медленнее.

Я подумал, что было бы здорово, если бы мы могли составить репозиторий «советов» по ​​улучшению производительности IOStreams, что работает, а что нет.

Вопросы для рассмотрения

  • буферизация (rdbuf()->pubsetbuf(buffer, size))
  • синхронизация (std::ios_base::sync_with_stdio)
  • обработка локали (можно ли использовать урезанную локаль или вообще удалить ее?)

Конечно, другие подходы приветствуются.

Примечание. Была упомянута "новая" реализация Дитмара Куля, но мне не удалось найти подробностей о ней. Предыдущие ссылки кажутся неработающими.


person Matthieu M.    schedule 02.03.2011    source источник
comment
Я делаю это вопросом часто задаваемых вопросов. Не стесняйтесь вернуться, если вы считаете, что это неправильно.   -  person sbi    schedule 02.03.2011
comment
@Matthieu: Дитмар однажды сказал, что его работа заброшена, хотя я не могу найти, где именно. (В общем, вам нужно поискать в группах новостей, чтобы найти этот материал. comp.lang.c++.moderated было местом, где происходили все интересные обсуждения C++ в 90-х.)   -  person sbi    schedule 02.03.2011
comment
Верен ли этот фактор и для g++? Кажется, я помню, что в реализации gnu stdlib была проделана работа, чтобы удалить ненужное снижение производительности. (Я редко делаю чувствительный к производительности форматированный ввод-вывод, поэтому я не знаю).   -  person AProgrammer    schedule 02.03.2011
comment
@sbi, я почти уверен, что он перестал над этим работать. Проблема недавно всплыла на clc++m, и он действительно участвовал.   -  person AProgrammer    schedule 02.03.2011
comment
@AProgrammer Разница в производительности - это, по сути, городская легенда, основанная на двух фактах: (1) Устаревшая реализация c++stdlib была медленнее. (2) Многие люди не знают о std::ios_base::sync_with_stdio.   -  person Konrad Rudolph    schedule 02.03.2011
comment
@AProgrammer: я констатировал падение производительности на 17% только при использовании gcc 3.4.2 в Unix после увеличения размера буфера.   -  person Matthieu M.    schedule 02.03.2011
comment
@Matthieu, спасибо за данные.   -  person AProgrammer    schedule 02.03.2011
comment
@AProgrammer: я предоставил код, который использовал для бенчмаркинга (полностью), меня интересуют результаты на других платформах, если у вас есть повод. Судя по моим измерениям, поведение по умолчанию в gcc/unix уже хорошо работает, и дополнительная настройка не требуется.   -  person Matthieu M.    schedule 02.03.2011
comment
@Konrad: Если я отлаживаю реализацию потоков Dinkumware (одну из наиболее широко распространенных) операторов ввода, я в конечном итоге приду к scanf(). Конечно, поскольку это разделяет все недостатки scanf() и добавляет несколько слоев поверх, эта реализация потока, в конечном счете, будет медленнее. И я говорю здесь не о дисковом вводе-выводе, а о чистом синтаксическом анализе. Теоретически потоки могут быть даже быстрее, чем printf()/scanf(), но я еще не встречал такой реализации в дикой природе.   -  person sbi    schedule 02.03.2011
comment
@AProgrammer: мой комментарий вводил в заблуждение. Да, он прекратил работу над этим много лет назад. Чего я не смог найти, так это публикации, где он объяснял, почему его работа так и не была принята.   -  person sbi    schedule 02.03.2011
comment
@sbi: та же проблема регулярно возникает в C ++, которую я обнаружил. Обычно программирование шаблонов может перемещать проверки из среды выполнения во время компиляции, но в большинстве случаев библиотека C++ представляет собой тонкую оболочку библиотеки C, которая в любом случае выполняет все проверки во время выполнения...   -  person Matthieu M.    schedule 02.03.2011
comment
Матье, я использовал тот же код, уменьшил количество итераций до 1, использовал большой файл данных и, используя время, увидел 2-3-кратную разницу между вашим тестом cpp и тестом c.   -  person Bogatyr    schedule 02.03.2011
comment
@sbi: у тебя все еще есть его работа? Я даже не смог найти его архивы, а его сайт, кажется, был перемещен/закрыт.   -  person Matthieu M.    schedule 02.03.2011
comment
@sbi, вот сообщение, о котором я думал: groups. google.com/group/comp.lang.c++.moderated/msg/   -  person AProgrammer    schedule 02.03.2011
comment
@Matthieu, ссылка в сообщении, на которое я ссылался выше, здесь жива.   -  person AProgrammer    schedule 02.03.2011
comment
@Matthieu: я был не обходным путем, а полномасштабной реализацией потоков, которая, как он утверждал (я никогда не пробовал), работает быстрее, чем C IO. Google нашел его по адресу dietmar-kuehl.de/cxxrt. Тем не менее, большинство исходных файлов имеют отметку времени 2002, некоторые 2003, так что они действительно устарели.   -  person sbi    schedule 02.03.2011
comment
@AProgrammer: Это не то сообщение, которое я искал, но это в значительной степени то содержание, которое я хотел. Спасибо за публикацию!   -  person sbi    schedule 02.03.2011
comment
@sbi: я не сказал обходной путь, но обходной путь, который можно где-то перевести на производстве, спасибо за ссылку, я положу это в свои вещи, чтобы прочитать :)   -  person Matthieu M.    schedule 02.03.2011
comment
@Matthieu: Ах, извините за неправильное понимание.   -  person sbi    schedule 02.03.2011


Ответы (3)


Вот что я собрал до сих пор:

Буферизация:

Если по умолчанию буфер очень мал, увеличение размера буфера определенно может улучшить производительность:

  • уменьшает количество обращений к жесткому диску
  • уменьшает количество системных вызовов

Буфер можно установить, обратившись к базовой реализации streambuf.

char Buffer[N];

std::ifstream file("file.txt");

file.rdbuf()->pubsetbuf(Buffer, N);
// the pointer reader by rdbuf is guaranteed
// to be non-null after successful constructor

Предупреждение @iavr: согласно cppreference лучше всего для вызова pubsetbuf перед открытием файла. В остальном различные реализации стандартных библиотек ведут себя по-разному.

Обработка региональных настроек:

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

Языковой стандарт C по умолчанию не предназначен для выполнения каких-либо преобразований, а также является единым для всех компьютеров. Это хорошее значение по умолчанию.

Синхронизация:

Я не увидел улучшения производительности при использовании этого средства.

Доступ к глобальному параметру (статическому элементу std::ios_base) можно получить с помощью статической функции sync_with_stdio.

Измерения:

Играя с этим, я играл с простой программой, скомпилированной с использованием gcc 3.4.2 на SUSE 10p3 с -O2.

C : 7.76532e+06
C++: 1.0874e+07

Что означает замедление примерно на 20%... для кода по умолчанию. Действительно, изменение буфера (в C или C++) или параметров синхронизации (C++) не дало никаких улучшений.

Результаты других:

@Irfy на g++ 4.7.2-2ubuntu1, -O3, виртуализированная Ubuntu 11.10, 3.5.0-25-универсальная, x86_64, достаточно оперативной памяти/процессора, 196 МБ нескольких запусков «find / >> largefile.txt»

C : 634572 C++: 473222

C++ на 25 % быстрее

@Matteo Italia на g++ 4.4.5, -O3, Ubuntu Linux 10.10 x86_64 со случайным файлом размером 180 МБ

C : 910390
C++: 776016

C++ на 17% быстрее

@Bogatyr на g++ i686-apple-darwin10-g++-4.2.1 (GCC) 4.2.1 (Apple Inc. build 5664), mac mini, 4 ГБ ОЗУ, простаивает, за исключением этого теста с файлом данных 168 МБ.

C : 4.34151e+06
C++: 9.14476e+06

C++ на 111% медленнее

@Asu на clang++ 3.8.0-2ubuntu4, Kubuntu 16.04 Linux 4.8-rc3, 8 ГБ оперативной памяти, i5 Haswell, Crucial SSD, файл данных 88 МБ (архив tar.xz)

C : 270895 C++: 162799

C++ на 66% быстрее

Итак, ответ таков: это проблема качества реализации, и она действительно зависит от платформы:/

Полный код здесь для тех, кто интересуется бенчмаркингом:

#include <fstream>
#include <iostream>
#include <iomanip>

#include <cmath>
#include <cstdio>

#include <sys/time.h>

template <typename Func>
double benchmark(Func f, size_t iterations)
{
  f();

  timeval a, b;
  gettimeofday(&a, 0);
  for (; iterations --> 0;)
  {
    f();
  }
  gettimeofday(&b, 0);
  return (b.tv_sec * (unsigned int)1e6 + b.tv_usec) -
         (a.tv_sec * (unsigned int)1e6 + a.tv_usec);
}


struct CRead
{
  CRead(char const* filename): _filename(filename) {}

  void operator()() {
    FILE* file = fopen(_filename, "r");

    int count = 0;
    while ( fscanf(file,"%s", _buffer) == 1 ) { ++count; }

    fclose(file);
  }

  char const* _filename;
  char _buffer[1024];
};

struct CppRead
{
  CppRead(char const* filename): _filename(filename), _buffer() {}

  enum { BufferSize = 16184 };

  void operator()() {
    std::ifstream file(_filename, std::ifstream::in);

    // comment to remove extended buffer
    file.rdbuf()->pubsetbuf(_buffer, BufferSize);

    int count = 0;
    std::string s;
    while ( file >> s ) { ++count; }
  }

  char const* _filename;
  char _buffer[BufferSize];
};


int main(int argc, char* argv[])
{
  size_t iterations = 1;
  if (argc > 1) { iterations = atoi(argv[1]); }

  char const* oldLocale = setlocale(LC_ALL,"C");
  if (strcmp(oldLocale, "C") != 0) {
    std::cout << "Replaced old locale '" << oldLocale << "' by 'C'\n";
  }

  char const* filename = "largefile.txt";

  CRead cread(filename);
  CppRead cppread(filename);

  // comment to use the default setting
  bool oldSyncSetting = std::ios_base::sync_with_stdio(false);

  double ctime = benchmark(cread, iterations);
  double cpptime = benchmark(cppread, iterations);

  // comment if oldSyncSetting's declaration is commented
  std::ios_base::sync_with_stdio(oldSyncSetting);

  std::cout << "C  : " << ctime << "\n"
               "C++: " << cpptime << "\n";

  return 0;
}
person Matthieu M.    schedule 02.03.2011
comment
На самом деле я обнаружил, что C++ быстрее (g++ 4.4.5, -O3, Ubuntu Linux 10.10 x86_64): со случайным файлом размером 180 МБ я получил C: 910390 C++: 776016. - person Matteo Italia; 02.03.2011
comment
@Matteo: Ах, это здорово. Мне нужно попробовать и с g++ 4.3.2. - person Matthieu M.; 02.03.2011
comment
Вопрос, который привел к этому, не имеет ничего общего с предпочтениями, он связан с конкретными измерениями типичной обработки входных данных. Ваш тест не очень интересен, так как он не соответствует реальному случаю. Вместо этого, почему бы вам не написать сценарий оболочки, который запускает вашу программу через 1 итерацию на наборе больших файлов и не измеряет совокупное время настенных часов. - person Bogatyr; 02.03.2011
comment
и, во-вторых, вам нужно разбить запуски на: 1 запуск случая C, 1 запуск случая C++, не помещая их оба вместе в один и тот же исполняемый файл. - person Bogatyr; 02.03.2011
comment
Хорошо, я запустил ваш код как есть, с результатами (3 итерации): C: 4.34151e+06 C++: 9.14476e+06, g++ i686-apple-darwin10-g++-4.2.1 (GCC) 4.2.1 (Apple Inc. , сборка 5664), mac mini, 4 ГБ ОЗУ, простаивает, за исключением этого теста. Мой файл данных 168MB - person Bogatyr; 02.03.2011
comment
@ Богатырь gettimeofday, если что-то более точнее, чем time. Кроме того, это является хорошим приближением к реальному случаю: чтению данных. В конце концов, мы не хотим измерять другие вещи, только чтение данных. Так что этот бенчмарк хороший. И размещение обоих кодов в одном исполняемом файле тоже прекрасно. Просто убедитесь, что выполняется достаточно итераций теста, чтобы компенсировать замедление прогрева (или запустите его один раз в начале, что делает Матье). Этот тест намного превосходит предложенные вами «улучшения». - person Konrad Rudolph; 02.03.2011
comment
@Konrad На одной итерации все в порядке, если файл имеет определенный размер. И мой интерес к этой теме проистекает из случая, когда мои улучшения были сценарием - соревнования алгоритмов, где у вас есть очень ограниченное время для чтения разных, больших наборов данных, а не одних и тех же данных. ставить снова и снова. Дело в том, что на том сайте по крайней мере в тот день cin›s сильно проиграл scanf. На моем mac mini с заявленным g++ тоже выигрывает scanf. Однако на моем Ubuntu linux vmware на ноутбуке с Windows 7 с 4.4.1 cin ›› s превосходит scanf. Так что пойди разберись, я согласен, что это зависит. - person Bogatyr; 02.03.2011
comment
@Bogatyr: я подозреваю, что разница связана с улучшениями в g++; Я не вижу изменений в реализации iostream между g++ 4.2 и 4.4, но я заметил, что они улучшили многие вещи в оптимизаторе, особенно в отношении встраивания; со всеми слоями, задействованными в iostream, я думаю, что изменения в алгоритмах встраивания действительно могут иметь существенное значение. - person Matteo Italia; 03.03.2011
comment
Я только что протестировал на 3 машинах Linux, скомпилированных с помощью g++ с 4.5.4 по 4.7.2, различия от C++ на 25% быстрее до C++ на 40% быстрее. - person Irfy; 20.03.2013
comment
Программа всегда запускает cread перед cppread, и они читают один и тот же файл. Тогда второй выиграет от дискового кеша, заполненного первым. - person musiphil; 16.10.2013
comment
@musiphil: обратите внимание, как реализован benchmark, есть первый (не рассчитанный по времени) пробный прогон для прогрева кеша, и только затем выполняется N прогонов (по времени). - person Matthieu M.; 16.10.2013
comment
@musiphil: я не жалуюсь, настолько легко иметь бессмысленную тестовую программу (из-за оптимизации, прогрева кеша, ...), что я благодарен за дополнительную пару глаз, внимательно изучающих этот код. - person Matthieu M.; 16.10.2013
comment
@Матье Хорошая работа. Я просто экспериментировал с чтением большого двоичного файла и искал способы управления буфером. Я понял, используя strace, что file.rdbuf()->pubsetbuf() в моем случае игнорировался. Затем я увидел здесь, что его следует вызывать до< /i> открытие файла, чего вы не делаете в тесте. - person iavr; 23.05.2015
comment
@iavr: Интересно, это похоже на ограничение libstdc++. Меня это немного раздражает, так как RAII всегда открывается первым... Думаю, после правильной упаковки он будет работать лучше. - person Matthieu M.; 23.05.2015
comment
но cppreference говорит, что file.rdbuf()->pubsetbuf(Buffer, N); в базовом классе ничего не делает - en.cppreference.com/w/ cpp/io/basic_streambuf/pubsetbuf - person hg_git; 30.08.2016
comment
@hg_git: В частности, cppreference упоминает, что реализация std::basic_streambuf::pubsetbuf ничего не делает, однако pubsetbuf является виртуальным методом и существует специально для того, чтобы производные классы могли (если они того пожелают) заставить его сделать что-то полезное. Оказывается, ifstream даст производную версию basic_streambuf, которая переопределяет pubsetbuf. - person Matthieu M.; 30.08.2016
comment
@MatthieuM. Спасибо :) где я могу узнать о том, что basic_streambuf переопределяет pubsetbuf? - person hg_git; 31.08.2016
comment
Файл ~100 МБ, clang version 3.8.0-2ubuntu4 компиляция с -Os: C: 278425, C++: 159543 — улучшение на 75%! Получение немного худших результатов на gcc, немного ускорение C и немного замедление C++, но с небольшим отрывом. - person Asu; 04.10.2016
comment
@Asu: gcc и clang по умолчанию используют разные стандартные библиотеки C++ (libstdc++ и libc++ соответственно), так что это может быть причиной разницы, которую вы наблюдаете. Спасибо за эту точку данных :) - person Matthieu M.; 04.10.2016
comment
@MatthieuM. - хороший момент - я попытался скомпилировать с clang + libstdc++ и получил C: 273557 - C++: 159604 Что на самом деле на удивление даже лучше стороны C++. g++ : C : 267510 - C++: 172379 Приятно видеть, как развивается clang. - person Asu; 04.10.2016
comment
Я фактически удалил синхронизацию stdio и буферизацию и не столкнулся со значительным влиянием на производительность. - person Asu; 04.10.2016

Еще два улучшения:

Выполните std::cin.tie(nullptr); перед интенсивным вводом/выводом.

Цитирование http://en.cppreference.com/w/cpp/io/cin:

После создания std::cin функция std::cin.tie() возвращает &std::cout, и аналогичным образом std::wcin.tie() возвращает &std::wcout. Это означает, что любая операция форматированного ввода в std::cin вызывает вызов std::cout.flush(), если какие-либо символы ожидают вывода.

Вы можете избежать очистки буфера, отвязав std::cin от std::cout. Это актуально для нескольких смешанных вызовов std::cin и std::cout. Обратите внимание, что вызов std::cin.tie(std::nullptr); делает программу непригодной для интерактивного запуска пользователем, поскольку вывод может быть задержан.

Соответствующий ориентир:

Файл test1.cpp:

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);

  int i;
  while(cin >> i)
    cout << i << '\n';
}

Файл test2.cpp:

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);
  cin.tie(nullptr);

  int i;
  while(cin >> i)
    cout << i << '\n';

  cout.flush();
}

Оба составлены g++ -O2 -std=c++11. Версия компилятора: g++ (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4 (да, я знаю, довольно старая).

Сравнительные результаты:

work@mg-K54C ~ $ time ./test1 < test.in > test1.in

real    0m3.140s
user    0m0.581s
sys 0m2.560s
work@mg-K54C ~ $ time ./test2 < test.in > test2.in

real    0m0.234s
user    0m0.234s
sys 0m0.000s

(test.in состоит из 1179648 строк, каждая из которых состоит только из одного 5. Это 2,4 МБ, извините, что не разместила его здесь.).

Я помню, как решал алгоритмическую задачу, где онлайн-судья отклонял мою программу без cin.tie(nullptr), но принимал ее с cin.tie(nullptr) или printf/scanf вместо cin/cout.

Используйте '\n' вместо std::endl.

Цитирование http://en.cppreference.com/w/cpp/io/manip/endl :

Вставляет символ новой строки в выходную последовательность os и сбрасывает ее, как если бы вызывал os.put(os.widen('\n')) с последующим os.flush().

Вы можете избежать сброса буфера, напечатав '\n' вместо endl.

Соответствующий ориентир:

Файл test1.cpp:

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);

  for(int i = 0; i < 1179648; ++i)
    cout << i << endl;
}

Файл test2.cpp:

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);

  for(int i = 0; i < 1179648; ++i)
    cout << i << '\n';
}

Оба скомпилированы, как указано выше.

Сравнительные результаты:

work@mg-K54C ~ $ time ./test1 > test1.in

real    0m2.946s
user    0m0.404s
sys 0m2.543s
work@mg-K54C ~ $ time ./test2 > test2.in

real    0m0.156s
user    0m0.135s
sys 0m0.020s
person Community    schedule 11.02.2016
comment
Ах да, ситуация endl обычно хорошо известна знатокам, но так много руководств используют ее по умолчанию (почему????), что она регулярно сбивает с толку начинающих/программистов среднего уровня. Что касается tie: сегодня я кое-чему учусь! Я знал, что подсказка пользователю вызовет сброс, но не знал, как это контролируется. - person Matthieu M.; 11.02.2016

Интересно, что вы говорите, что программисты C предпочитают printf при написании C++, поскольку я вижу много кода, написанного на C, кроме использования cout и iostream для записи вывода.

Пользователи часто могут повысить производительность, используя filebuf напрямую (Скотт Мейерс упомянул об этом в «Эффективном STL»), но существует относительно мало документации по прямому использованию filebuf, и большинство разработчиков предпочитают std::getline, который в большинстве случаев проще.

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

Недавно я видел здесь еще одну тему по этому поводу, так что она близка к дублированию.

person CashCow    schedule 02.03.2011
comment
Если вы получаете лучшую производительность, используя файловый буфер напрямую, то это означает, что код синтаксического анализа (во всяком случае, для чтения) является причиной производительности, поскольку это то, чем std::istream оборачивает буфер. К сожалению, широко распространенные реализации потоков ввода-вывода используют printf()/scanf() под капотом, что, безусловно, должно быть медленнее, чем прямое использование ввода-вывода C std lib. (Также см. мой комментарий к @Konrad по этому вопросу.) - person sbi; 02.03.2011
comment
код, который является C, кроме использования cout и iostream - мы называем его C с iostreams, и это то, что проходит за C++ во многих университетских курсах. - person MaHuJa; 23.10.2011