Блог пользователя andreyv

Автор andreyv, 12 лет назад, По-русски

Всем привет!

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

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 медленнее — наверняка из-за необходимости динамически выделять память.

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 такое поведение включено по умолчанию и не имеет вышеописанных ограничений.

  • Проголосовать: нравится
  • +63
  • Проголосовать: не нравится

»
12 лет назад, # |
Rev. 3   Проголосовать: нравится +4 Проголосовать: не нравится

Примечательно, что printf()/scanf() обгоняют даже самописные функции

gets быстрее

Читать int посимвольно, используя getc, быстрее.

С выводом история аналогичная.

Или имелось в виду что-то другое? :-)

UPD: Забыл код прикрепить. Это шаблон, который я использую на олимпиадах link

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +8 Проголосовать: не нравится

    Спасибо. Теперь функции выглядят так: http://pastie.org/4675492 Однако они всё равно оставались медленнее printf()/scanf(), пока я не включил _CRT_DISABLE_PERFCRIT_LOCKS (см. дополнение к посту).

    • »
      »
      »
      12 лет назад, # ^ |
      Rev. 5   Проголосовать: нравится +3 Проголосовать: не нравится

      Как я понял, дополнение относится к MSVC.

      А на g++? У меня и под windows, и под linux под разными версиями от 3.2.x до последних некоторое заметное ускорение всегда было...

      UPD: Извиняюсь, нашел ответ сам. Условие задач и не только нужно читать до конца :-D

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

      • »
        »
        »
        »
        12 лет назад, # ^ |
        Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

        На MinGW/g++ всё изначально хорошо: вывод int вручную работает в 3,5 раза быстрее printf(), ввод — в ~6 раз быстрее scanf().

        UPD: Извиняюсь, слишком долго печатал ответ ;-)

»
12 лет назад, # |
  Проголосовать: нравится -8 Проголосовать: не нравится

Зато iostream банально удобнее.

»
12 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится
  1. А можно для сравнения fwrite/fread с самописным парсером?
  2. Под виндой есть известная фича с тем, что жесткий диск кешируется в свободной оперативке. Из-за этого результаты могут быть совершенно не показательны.
  • »
    »
    12 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

     2. Что значит «не показательны»? Мы ведь и хотим измерить скорость выполнения программного кода, а не скорость файловой системы/диска. В таком случае запись/чтение файла из оперативной памяти как раз-таки и есть самый желательный случай. (Кстати, хорошая идея — надо будет попробовать с ramdisk'ом.)

»
12 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

С double наоборот: iostreams заметно тормозит

Это потому что MinGW — под венду GCC немного не того. Я уже писал об это здесь.

На оригинальном GCC (под Linux) разница не в 5-6 раз как у Вас, а на 20-30%, как и можно ожидать. Вот так выглядит результат Вашей проги у меня: http://pastie.org/4698235

»
9 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Can you tell me about custom/in or custom/out. What it means? Thank you.

»
9 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

(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).

»
8 лет назад, # |
  Проголосовать: нравится +1 Проголосовать: не нравится

Is ios::sync_with_stdio(0); the same as ios_base::sync_with_stdio(0)?

  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится +9 Проголосовать: не нравится

    Do you mean std::basic_ios? Yes, it is inherited from the std::ios_base class.

»
5 лет назад, # |
Rev. 2   Проголосовать: нравится +5 Проголосовать: не нравится

Here are my results, using Ubuntu 18.04, 3.8GHz i7-7700HQ CPU, g++ 7.4.0 (compiled with -std=c++11 since gets is removed from c++14):

int, printf        0.59   0.61   0.59
int, cout          0.52   0.52   0.52
int, custom/out    0.27   0.27   0.27
int, scanf         0.77   0.77   0.79
int, cin           0.73   0.71   0.74
int, custom/in     0.17   0.18   0.17
double, printf     4.58   4.52   4.67
double, cout       5.43   5.47   5.47
double, scanf      1.59   1.57   1.44
double, cin        2.84   2.54   2.74
char, putchar      0.33   0.34   0.32
char, cout         0.65   0.64   0.64
char, getchar      0.22   0.21   0.22
char, cin          0.57   0.56   0.55
char *, printf     0.54   0.53   0.54
char *, puts       0.23   0.23   0.24
char *, cout       0.43   0.41   0.42
string, cout       0.41   0.42   0.44
char *, scanf      0.65   0.61   0.62
char *, gets       0.21   0.20   0.21
char *, cin        0.20   0.21   0.20
string, getline    0.41   0.41   0.39

andreyv Do you mind if I modify this code (with attribution) for testing? If possible what license do you release this code under?

»
5 лет назад, # |
  Проголосовать: нравится +5 Проголосовать: не нравится

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.

»
5 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

awesome post!