Большая разница (x9) во время выполнения между почти идентичным кодом в C и С++

Я пытался решить это упражнение с сайта www.spoj.com: FCTRL - Factorial

Вам действительно не нужно его читать, просто сделайте это, если вам интересно:)

Сначала я реализовал его в С++ (вот мое решение):

#include <iostream>
using namespace std;

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    std::ios_base::sync_with_stdio(false); // turn off synchronization with the C library’s stdio buffers (from /info/67161/difference-in-execution-time-in-c-and-c/457941#457941)

    cin >> num_of_inputs;

    while (num_of_inputs--)
    {
        cin >> fact_num;

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        cout << num_of_trailing_zeros << "\n";
    }

    return 0;
}

Я загрузил его как решение для g++ 5.1

Результат: Time 0.18 Mem 3.3M Результаты выполнения С++

Но потом я увидел несколько комментариев, в которых утверждалось, что их время исполнения было меньше 0,1. Поскольку я не мог думать о более быстром алгоритме, я попытался реализовать тот же код в C:

#include <stdio.h>

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    scanf("%d", &num_of_inputs);

    while (num_of_inputs--)
    {
        scanf("%d", &fact_num);

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        printf("%d", num_of_trailing_zeros);
        printf("%s","\n");
    }

    return 0;
}

Я загрузил его как решение для gcc 5.1

На этот раз результат: Time 0.02 Mem 2.1M Результаты выполнения C

Теперь код почти тот же, я добавил std::ios_base::sync_with_stdio(false); в код С++, как было предложено здесь, чтобы отключить синхронизацию с буферами stdio в библиотеках C. Я также разделил printf("%d\n", num_of_trailing_zeros); на printf("%d", num_of_trailing_zeros); printf("%s","\n");, чтобы компенсировать двойной вызов operator<< в cout << num_of_trailing_zeros << "\n";.

Но я все еще видел x9 лучшую производительность и уменьшал использование памяти в коде C и С++.

Почему это?

ИЗМЕНИТЬ

Я зафиксировал unsigned long до unsigned int в коде C. Это должно быть unsigned int, а результаты, которые показаны выше, связаны с новой версией (unsigned int).

Ответ 1

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

сканирование ввода с помощью scanf("%d", &fact_num); с одной стороны и cin >> fact_num; с другой стороны не кажется очень дорогостоящим в любом случае. На самом деле он должен быть менее дорогостоящим в С++, поскольку тип преобразования известен во время компиляции, и правильный парсер может быть вызван непосредственно компилятором С++. То же самое относится к выходу. Вы даже можете написать отдельный вызов для printf("%s","\n");, но компилятор C достаточно хорош, чтобы скомпилировать его как вызов putchar('\n');.

Поэтому, глядя на сложность ввода-вывода и вычислений, версия С++ должна быть быстрее, чем версия C.

Полностью отключение буферизации stdout замедляет реализацию C до чего-то еще медленнее, чем версия С++. Еще одно испытание AlexLop с fflush(stdout); после последнего printf дает аналогичную производительность, как и версия С++. Это не так медленно, как полностью отключить буферизацию, потому что вывод записывается в систему небольшими фрагментами вместо одного байта за раз.

Это, по-видимому, указывает на конкретное поведение в вашей библиотеке С++: я подозреваю, что ваша системная реализация cin и cout очищает вывод до cout при запросе ввода от cin. Некоторые библиотеки C тоже делают это, но обычно только при чтении/записи на терминал и обратно. Бенчмаркинг, выполненный сайтом www.spoj.com, вероятно, перенаправляет входные и выходные данные в файлы и из них.

AlexLop провела еще один тест: одновременно считывая все входы в векторе, а затем вычисляя и записывая весь вывод, вы понимаете, почему версия С++ настолько медленнее. Это повышает производительность до версии C, это доказывает мою точку зрения и устраняет подозрение на код форматирования С++.

Еще один тест от Blastfurnace, сохраняющий все выходы в std::ostringstream и покраснение, что в одном взрыве в конце, улучшает производительность С++ до уровня базовой версии C. Что и требовалось доказать.

Вмешающий ввод из cin и вывод на cout, по-видимому, вызывает очень неэффективную обработку ввода-вывода, нарушая схему буферизации потока. снижая производительность в 10 раз.

PS: ваш алгоритм неверен для fact_num >= UINT_MAX / 5, потому что fives *= 5 будет переполняться и обертываться до того, как он станет > fact_num. Вы можете исправить это, сделав fives a unsigned long или unsigned long long, если один из этих типов больше, чем unsigned int. Также используйте %u как формат scanf. Вам повезло, что ребята на www.spoj.com не слишком строги в своих тестах.

РЕДАКТИРОВАТЬ: Как объясняется позже, vitaux, это поведение действительно обеспечивается стандартом С++. cin по умолчанию привязан к cout. Операция ввода из cin, для которой требуется перезарядка входного буфера, вызовет cout сбросить ожидающий вывод. В реализации OP cin систематически стирает cout, что немного перегружает и заметно неэффективно.

Илья Попов обеспечил простое решение для этого: cin можно отвязать от cout, добавив еще одно магическое заклинание в дополнение к std::ios_base::sync_with_stdio(false);:

cin.tie(nullptr);

Также обратите внимание, что такой принудительный сброс также возникает при использовании std::endl вместо '\n' для создания конца строки на cout. Изменение выходной строки на более идиоматический и невинно выглядящий cout << num_of_trailing_zeros << endl; С++ будет одинаково снижать производительность.

Ответ 2

Другим трюком, чтобы сделать iostream быстрее, когда вы используете как cin, так и cout, является вызов

cin.tie(nullptr);

По умолчанию при вводе чего-либо из cin он стирает cout. Это может значительно повредить производительность, если вы выполняете чередование ввода и вывода. Это делается для использования интерфейса командной строки, где вы показываете приглашение, а затем ждете данных:

std::string name;
cout << "Enter your name:";
cin >> name;

В этом случае вы хотите убедиться, что приглашение действительно отображается до того, как вы начнете ждать ввода. Если линия выше вы нарушаете эту связь, cin и cout становятся независимыми.

Начиная с С++ 11, еще один способ повысить производительность iostreams - использовать std::getline вместе с std::stoi, например:

std::string line;
for (int i = 0; i < n && std::getline(std::cin, line); ++i)
{
    int x = std::stoi(line);
}

Этот способ может приближаться к C-стилю в производительности или даже превосходить scanf. Использование getchar и особенно getchar_unlocked вместе с рукописным разбором по-прежнему обеспечивает лучшую производительность.

PS. Я написал сообщение, сравнивая несколько способов ввода чисел на С++, полезно для онлайн-судей, но это только на русском языке, извините. Однако образцы кода и итоговая таблица должны быть понятны.

Ответ 3

Проблема заключается в том, что, цитируя cppreference:

любой вход из std:: cin, вывод в std:: cerr или завершение программы заставляет вызов std:: cout.flush()

Это легко проверить: если вы замените

cin >> fact_num;

с

scanf("%d", &fact_num);

и то же самое для cin >> num_of_inputs, но сохраните cout, вы получите почти такую ​​же производительность в своей версии на С++ (или, скорее, в версии IOStream), как в C one:

введите описание изображения здесь

То же самое происходит, если вы сохраните cin, но замените

cout << num_of_trailing_zeros << "\n";

с

printf("%d", num_of_trailing_zeros);
printf("%s","\n");

Простым решением является развязка cout и cin, как упоминал Илья Попов:

cin.tie(nullptr);

Стандартным реализациям библиотек разрешено пропускать вызов в определенных случаях, но не всегда. Здесь цитата из С++ 14 27.7.2.1.3 (спасибо chqrlie):

Класс basic_istream:: sentry: во-первых, если is.tie() не является нулевым указателем, функция вызывает is.tie() → flush() для синхронизации выходной последовательности с любым связанным внешним потоком C. За исключением того, что этот вызов может быть подавлен, если область put is.tie() пуста. Дальнейшему выполнению разрешено отсрочить вызов для сброса до тех пор, пока не произойдет вызов is.rdbuf() → underflow(). Если такой вызов не происходит до того, как объект-сторож будет уничтожен, вызов для очистки может быть полностью исключен.