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

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

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

Принимая во внимание на этот вопрос:

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

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

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

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

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

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

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

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

Обработка локали:

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

По умолчанию 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, достаточно ram/cpu, 196 МБ из нескольких "find / >> largefile.txt" работает

C: 634572 C++: 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., сборка 5664), mac mini, 4 ГБ оперативной памяти, в режиме ожидания, за исключением этого теста с файлом данных объемом 168 МБ

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

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

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

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

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

  return 0;
}

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

вопрос 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

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

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

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

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

Другие вопросы по тегам