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