Всем привет!
Хочу продолжить тему скорости ввода/вывода в C++, которую когда-то начал товарищ freopen. freopen сравнивал производительность двух способов ввода/вывода в C++: унаследованной от C библиотеки stdio (<cstdio>
) и более новой библиотеки iostreams (<iostream>
/…). Однако в этих тестах не было учтено, что iostreams можно значительно ускорить, включив некоторые оптимизации. Об их существовании уже неоднократно упоминалось на Codeforces (раз, два, три). Сейчас я написал софт, который сравнивает производительность stdio и iostreams на стандартном вводе/выводе с учётом этого.
UPD1: Добавлена информация про _CRT_DISABLE_PERFCRIT_LOCKS
.
UPD2: Для полноты картины добавлены вызовы printf()
/scanf()
на строках.
Что это за оптимизации?
Первая состоит в том, что в начале программы, перед каким-либо вводом/выводом, можно вставить строчку
ios_base::sync_with_stdio(false);
Эта команда отключает синхронизацию iostreams с stdio (описание). По умолчанию она включена, то есть, iostreams и stdio можно использовать вместе на одном и том же потоке, перемежая их вызовы. После отключения синхронизации так делать больше нельзя, однако за счёт этого iostreams может работать быстрее.
Вторая оптимизация заключается в том, что для cin
можно выключить привязку к cout
:
cin.tie(NULL);
По умолчанию cin
привязан к cout
, что означает, что перед каждой операцией над cin
сначала сбрасывается буфер cout
(описание). Отключение этой функции опять-таки позволяет iostreams работать быстрее. С этой оптимизацией надо быть внимательным в интерактивных задачах: либо не использовать её, либо не забывать явный flush
.
Отмечу ещё, что на производительность iostreams негативно влияет частое использование endl
, поскольку endl
не только выводит символ новой строки, но и сбрасывает буфер потока (описание). Вместо endl
можно просто выводить '\n'
или "\n"
.
Какие тесты включены в программу?
Я постарался сымитировать наиболее типичные ситуации, возникающие при решении задач.
- Ввод/вывод
int
с помощью stdio, iostreams и, для сравнения, самопальных функций - Ввод/вывод
double
- Посимвольный ввод/вывод
- Ввод/вывод строк: как с
char *
, так и сstd::string
Какие тесты не включены в программу?
long long
— полагаю, результат будет коррелировать сint
- Преобразование
int
и запись результата в собственный буфер достаточно большого размера, а затем прямой вывод буфера с помощьюfwrite()
/cout.write()
(и аналогичное решение для ввода) - Экзотический способ посимвольного ввода/вывода:
cin.rdbuf()->sgetc()
иcout.rdbuf()->sputc()
- Какие-либо манипуляции с размером буфера потоков (похоже, что в GCC iostreams для стандартных потоков игнорирует пользовательские установки насчёт этого). Это направление ещё можно исследовать потом.
Как это запустить у себя?
Надо скомпилировать программу, не забыв включить оптимизацию (-O2
/Release), и запустить её с тем же рабочим каталогом, где она и находится. Если при запуске в Windows появляется сообщение Access denied, то может помочь запуск программы с повышенными правами. Программе потребуется пара сотен мегабайтов свободного места в каталоге для временных файлов.
Дополнительные замечания
- Почему нужно для каждого теста создавать новый процесс?
Из-за ios_base::sync_with_stdio(false)
, который препятствует поочерёдному тестированию stdio и iostreams, а также (теоретически) мешает использовать freopen()
, чтобы перенаправлять cin
/cout
.
- Зачем удалять файл от предыдущего запуска перед каждым тестом?
Чтобы все запуски были в равных условиях. Хотя, это несколько спорный вопрос. Может, лучше не удалять, а переписывать?
- Почему время измеряет запускаемый, а не запускающий процесс?
Чтобы исключить время запуска/завершения процесса.
- Почему бы вместо
clock()
не использовать более точные вызовы, например,getrusage()
?
Это можно. Но сначала мне надо понять, как это делать в Windows :-)
Результаты
Запускал на компьютере с Pentium 4, так что время может показаться несколько большим.
- Для Visual C++ 2010: http://pastie.org/4680309
int, printf 9.45 9.48 9.44 int, cout 22.03 22.01 22.21 int, custom/out 11.17 11.06 11.20 int, scanf 5.04 4.77 4.82 int, cin 20.26 20.16 20.16 int, custom/in 10.25 10.25 10.25 double, printf 19.23 18.98 18.95 double, cout 37.49 37.52 37.44 double, scanf 12.11 11.75 11.73 double, cin 26.88 26.57 26.57 char, putchar 13.29 13.76 13.48 char, cout 23.52 24.15 23.41 char, getchar 12.87 12.82 12.74 char, cin 16.13 16.22 16.50 char *, printf 6.88 6.74 6.57 char *, puts 3.95 3.82 3.95 char *, cout 6.36 6.32 6.43 string, cout 6.40 6.40 6.61 char *, scanf 6.16 6.10 6.13 char *, gets 3.98 3.96 3.96 char *, cin 8.72 8.91 8.85 string, getline 11.70 11.47 11.53
Здесь всё очевидно: stdio значительно превосходит по скорости iostreams. Примечательно, что на int
printf()
/scanf()
быстрее даже самописных функций (однако см. дополнение ниже). Ввод/вывод строк с помощью puts()
/gets()
быстрее, чем с помощью printf()
/scanf()
— ну, это и понятно. Запись std::string
занимает столько же, сколько и запись char *
, а вот чтение в std::string
медленнее — наверняка из-за необходимости динамически выделять память.
- Для MinGW (GCC 4.7.0): http://pastie.org/4680314
int, printf 9.72 9.61 9.61 int, cout 6.08 6.05 6.10 int, custom/out 2.73 2.75 2.76 int, scanf 5.01 5.01 5.01 int, cin 3.99 4.04 4.04 int, custom/in 0.86 0.86 0.87 double, printf 22.51 22.40 22.42 double, cout 110.98 111.77 111.01 double, scanf 12.18 12.20 12.17 double, cin 118.87 118.84 118.87 char, putchar 1.67 1.65 1.64 char, cout 3.93 3.87 3.85 char, getchar 0.78 0.80 0.80 char, cin 3.29 3.31 3.29 char *, printf 5.55 5.47 5.49 char *, puts 5.37 5.32 5.41 char *, cout 8.72 8.72 8.78 string, cout 8.74 8.71 9.06 char *, scanf 7.07 7.04 7.02 char *, gets 3.84 3.79 3.77 char *, cin 5.30 5.38 5.35 string, getline 14.15 14.12 14.16
А здесь всё уже не так однозначно. Неожиданно оказывается, что iostreams на 20-30% быстрее stdio на int
. Правда, самописные функции значительно обгоняют и то, и то. С double
наоборот: iostreams заметно тормозит. Посимвольный ввод/вывод с putchar()
/getchar()
работает в 2-3 раза быстрее cout
/cin
. Ввод/вывод строк отличается не так сильно, но stdio и тут быстрее. На строках puts()
/gets()
также быстрее, чем printf()
/scanf()
. std::string
, как и в предыдущем случае, работает одинаково с char *
на выводе, однако медленнее на вводе.
Какие делать выводы и что использовать, пусть каждый решает сам. В комментариях приветствуется флейм конструктивное обсуждение.
Дополнение
В Visual C++ есть способ значительно ускорить базовые операции с потоками stdio, отключив блокировку потоков для функций getchar()
, putchar()
и некоторых других. Для этого надо перед всеми #include
вставить строчку
#define _CRT_DISABLE_PERFCRIT_LOCKS
(описание). Это сработает только при выполнении следующих дополнительных условий:
- Программа статически компонуется со стандартной библиотекой (
/MT
; на Codeforces, похоже, это так) - Программа включает
<stdio.h>
, но не включает ни<cstdio>
, ни какой-либо файл из библиотеки iostreams (<iostream>
/…)
Вместо всей этой магии можно также просто использовать _putchar_nolock()
/_getchar_nolock()
вместо putchar()
/getchar()
. В Linux тоже есть похожие функции: ссылка.
С этой оптимизацией посимвольный ввод/вывод ускоряется в восемь-девять (!) раз, а вместе с ним и вручную написанные функции ввода/вывода int
:
int, custom/out 1.70 1.70 1.72 int, custom/in 1.28 1.26 1.28 char, putchar 1.72 1.62 1.61 char, getchar 1.36 1.34 1.36
В MinGW такое поведение включено по умолчанию и не имеет вышеописанных ограничений.
gets быстрее
Читать int посимвольно, используя getc, быстрее.
С выводом история аналогичная.
Или имелось в виду что-то другое? :-)
UPD: Забыл код прикрепить. Это шаблон, который я использую на олимпиадах link
Спасибо. Теперь функции выглядят так: http://pastie.org/4675492 Однако они всё равно оставались медленнее
printf()
/scanf()
, пока я не включил_CRT_DISABLE_PERFCRIT_LOCKS
(см. дополнение к посту).Как я понял, дополнение относится к MSVC.
А на g++? У меня и под windows, и под linux под разными версиями от 3.2.x до последних некоторое заметное ускорение всегда было...
UPD: Извиняюсь, нашел ответ сам. Условие задач и не только нужно читать до конца :-D
На MinGW/g++ всё изначально хорошо: вывод
int
вручную работает в 3,5 раза быстрееprintf()
, ввод — в ~6 раз быстрееscanf()
.UPD: Извиняюсь, слишком долго печатал ответ ;-)
Зато
iostream
банально удобнее.далеко не всегда
2. Что значит «не показательны»? Мы ведь и хотим измерить скорость выполнения программного кода, а не скорость файловой системы/диска. В таком случае запись/чтение файла из оперативной памяти как раз-таки и есть самый желательный случай. (Кстати, хорошая идея — надо будет попробовать с ramdisk'ом.)
Это потому что MinGW — под венду GCC немного не того. Я уже писал об это здесь.
На оригинальном GCC (под Linux) разница не в 5-6 раз как у Вас, а на 20-30%, как и можно ожидать. Вот так выглядит результат Вашей проги у меня: http://pastie.org/4698235
Can you tell me about custom/in or custom/out. What it means? Thank you.
Ctrl+F "custom" in the program linked in the post.
(I see that this is kinda old entry, but it was brought up into recent actions I will make my input).
I think it would also be interesting to have results for different degrees of precisions "not set" (which is probably always a bad idea to use at contests) vs small (e.g. 2) vs big (e.g. 10).
Is
ios::sync_with_stdio(0);
the same asios_base::sync_with_stdio(0)
?Do you mean
std::basic_ios
? Yes, it is inherited from thestd::ios_base
class.Here are my results, using Ubuntu 18.04, 3.8GHz i7-7700HQ CPU, g++ 7.4.0 (compiled with
-std=c++11
sincegets
is removed from c++14):andreyv Do you mind if I modify this code (with attribution) for testing? If possible what license do you release this code under?
Sure. This code is in public domain.
Also tentative evidence that (CodeForces compilers) GNU C++17 7.3.0 I/O is faster than GNU C++11 5.1.0 I/O.
awesome post!