Подтвердить что ты не робот

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

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

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

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

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

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

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

Точки для рассмотрения

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

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

Примечание: упоминалась "новая" реализация, Dietmar Kuhl, но мне не удалось найти много деталей об этом. Предыдущие ссылки кажутся мертвыми.

4b9b3361

Ответ 1

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

Буферизация

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

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

Буфер можно установить, обратившись к базовой реализации 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, прежде чем открывать файл. Различные реализации стандартной библиотеки в противном случае имеют разные типы поведения.

Локальная обработка:

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

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

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

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

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

Размеры:

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

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

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

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

@Irfy на g++ 4.7.2-2ubuntu1, -O3, виртуализированный Ubuntu 11.10, 3.5.0-25-generic, x86_64, достаточно ram/cpu, 196MB нескольких "find/ → largefile.txt" работает

C: 634572 С++: 473222

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

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

C: 910390
С++: 776016

С++ на 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
С++: 9.14476e + 06

С++ 111% медленнее

@Asu on clang++ 3.8.0-2ubuntu4, Kubuntu 16.04 Linux 4.8-rc3, 8GB RAM, i5 Haswell, Crucial SSD, файл данных 88MB (архив tar.xz)

C: 270895 С++: 162799

С++ на 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 declaration is commented
  std::ios_base::sync_with_stdio(oldSyncSetting);

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

  return 0;
}

Ответ 2

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

Проблема 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 (да, я знаю, довольно старый).

Результаты тестов:

[email protected] ~ $ time ./test1 < test.in > test1.in

real    0m3.140s
user    0m0.581s
sys 0m2.560s
[email protected] ~ $ 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';
}

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

Результаты тестов:

[email protected] ~ $ time ./test1 > test1.in

real    0m2.946s
user    0m0.404s
sys 0m2.543s
[email protected] ~ $ time ./test2 > test2.in

real    0m0.156s
user    0m0.135s
sys 0m0.020s

Ответ 3

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

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

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

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