Как заставить IOStream работать лучше? - PullRequest
61 голосов
/ 02 марта 2011

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

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

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

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

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

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

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

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

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

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

Ответы [ 3 ]

47 голосов
/ 02 марта 2011

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

Буферизация

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

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

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

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

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

Можно получить доступ к настройке global (статический член 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

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

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

C: 910390
С ++: 776016

C ++ 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

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

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

C: 270895 С ++: 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;
}
15 голосов
/ 11 февраля 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
1 голос
/ 02 марта 2011

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

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

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

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

...