Велика різниця (x9) у часі виконання між майже однаковим кодом на C та C ++


85

Я намагався вирішити цю вправу з www.spoj.com: FCTRL - Factorial

Вам не обов’язково читати це, просто зробіть це, якщо вам цікаво :)

Спочатку я реалізував це на C ++ (ось моє рішення):

#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 https://stackoverflow.com/a/22225421/5218277)

    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

Результат: Час 0,18 Mem 3,3M Результати виконання C ++

Але потім я побачив деякі коментарі, які стверджували, що час їх виконання був менше 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

Цього разу результат був: Час 0,02 Мем 2,1 М C результати виконання

Тепер код майже такий самий , я додав std::ios_base::sync_with_stdio(false);до коду C ++, як було запропоновано тут, щоб вимкнути синхронізацію з буферами 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 та C ++.

Чому так?

РЕДАГУВАТИ

Я встановив , unsigned longщоб unsigned intв коді C. Це мало бути, unsigned intі результати, які наведені вище, пов’язані з новою ( unsigned int) версією.


31
Потоки C ++ надзвичайно повільні за дизайном. Тому що повільний та стійкий виграє перегони. : P ( бігає до того, як мене
спалахують

7
Повільність не пов’язана з безпекою чи пристосованістю. Це занадто розроблено з усіма прапорами потоку.
Karoly Horvath

8
@AlexLop. Використання a std::ostringstreamдля накопичення вихідних даних і надсилання його std::cout всім відразу в кінці зменшує час до 0,02. Використання std::coutв циклі просто повільніше в їх середовищі, і я не думаю, що існує простий спосіб його покращити.
Blastfurnace

6
Нікого більше не турбує той факт, що ці терміни були отримані за допомогою Ideone?
ildjarn

6
@Olaf: Боюсь, я не погоджуюсь, такий тип запитань дуже актуальний для всіх вибраних тегів. Взагалі C та C ++ досить близькі, що така різниця в продуктивності вимагає пояснень. Я радий, що ми його знайшли. Можливо, слід покращити GNU libc ++.
chqrlie

Відповіді:


56

Обидві програми роблять абсолютно те саме. Вони використовують той самий точний алгоритм, і, враховуючи його низьку складність, їх продуктивність здебільшого пов'язана з ефективністю обробки вхідних та вихідних даних.

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

Отже, дивлячись на складність введення / виводу та обчислень, версія С ++ повинна бути швидшою, ніж версія С.

Повністю відключаючи буферизацію stdoutсповільнює реалізацію C до чогось навіть повільнішого, ніж версія C ++. Черговий тест AlexLop з fflush(stdout);останнім результатом printfдає подібну продуктивність, як версія С ++. Це не так повільно, як повне вимкнення буферизації, оскільки вихідні дані записуються в систему невеликими шматками замість одного байта за раз.

Здається, це вказує на специфічну поведінку у вашій бібліотеці C ++: Я підозрюю, що ваша система реалізує cinта coutзмиває вихідні дані, coutколи запитується введення cin. Деякі бібліотеки C також роблять це, але зазвичай лише під час читання / запису на термінал та з нього. Бенчмаркінг, проведений на сайті www.spoj.com, ймовірно, перенаправляє вхід і вихід у файли та з них.

AlexLop зробив ще один тест: читання всіх входів відразу у векторі, а згодом обчислення та запис усіх вихідних даних допомагає зрозуміти, чому версія С ++ набагато повільніша. Це підвищує продуктивність до версії C, це підтверджує мою думку і знімає підозру щодо форматування коду C ++.

Черговий тест Blastfurnace, який зберігає всі вихідні дані std::ostringstreamта змиває це в одному вибуху в кінці, покращує продуктивність С ++ до базової версії С. QED.

Переплетення вхідних даних cinта вихідних даних, coutздається, спричиняє дуже неефективну обробку вводу-виводу, перешкоджаючи схемі буферизації потоку. зниження продуктивності в 10 разів.

PS: ваш алгоритм неправильний, fact_num >= UINT_MAX / 5тому що він fives *= 5буде переповнюватися і обертатися, перш ніж він стане > fact_num. Ви можете виправити це шляхом або якщо один з цих типів більше . Також використовувати як формат. Вам пощастило, хлопці з www.spoj.com не надто суворі у своїх тестах.fivesunsigned longunsigned long longunsigned int%uscanf

РЕДАКТУВАТИ: Як пізніше пояснив vitaux, ця поведінка справді передбачена стандартом C ++. за замовчуванням cinприв’язано до cout. Операція введення, cinдля якої вхідний буфер потребує поповнення, призведе coutдо змивання очікуваного виводу. Здійснюючи ОП, cinздається, coutсистематично змивається , що є трохи надмірним та помітно неефективним.

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

cin.tie(nullptr);

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


2
Ви, мабуть, маєте рацію щодо промивання потоком. Збираючи вихідні дані в a std::ostringstreamі виводячи все це один раз в кінці, час зводиться до паритету з версією C.
Blastfurnace

2
@ DavidC.Rankin: Я наважився на здогадку (cout зникає при читанні cin), придумав спосіб довести це, AlexLop реалізував це, і це дає переконливі докази, але Blastfurnace придумав інший спосіб довести свою думку і свої тести надати однаково переконливі свідчення. Я сприймаю це як доказ, але, звичайно, це не зовсім формальний доказ, дивлячись на вихідний код C ++.
chqrlie

2
Я спробував використовувати ostringstreamдля виводу, і це дало час 0,02 QED :). Щодо fact_num >= UINT_MAX / 5, ДОБРОГО пункту!
Алекс Лоп.

1
Збір усіх вхідних даних в a, vectorа потім обробка обчислень (без ostringstream) дає однаковий результат! Час 0,02. Поєднання і того, vectorі іншого ostringstreamне покращує. Той самий час 0,02
Алекс Лоп.

2
Більш просте виправлення, яке працює, навіть якщо sizeof(int) == sizeof(long long)це таке: додайте тест в тілі циклу після того, num_of_trailing_zeros += fact_num/fives;щоб перевірити, чи fivesдосягнув свого максимуму:if (fives > UINT_MAX / 5) break;
chqrlie

44

Ще одна хитрість , щоб зробити iostreams швидше , коли ви використовуєте як cinі coutце виклик

cin.tie(nullptr);

За замовчуванням, коли ви вводите щось із cin, воно зникає cout. Це може суттєво зашкодити продуктивності, якщо ви виконуєте чергування вводу та виводу. Це робиться для використання інтерфейсу командного рядка, де ви показуєте підказку, а потім чекаєте даних:

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

У цьому випадку ви хочете переконатися, що підказка насправді відображається, перш ніж почати чекати введення. За допомогою лінії вище ви розірвете цю краватку cinі coutстанете незалежними.

Починаючи з C ++ 11, ще одним способом досягнення кращої продуктивності з потоками iostream є використання std::getlineразом з std::stoi, наприклад, такого:

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

Цей спосіб може наблизитися до стилю С за характеристиками або навіть перевершити scanf. Використання, getcharособливо getchar_unlockedразом із рукописним синтаксичним аналізом, все ще забезпечує кращу продуктивність.

PS. Я написав допис, в якому порівнював кілька способів введення цифр на C ++, корисних для онлайн-суддів, але це лише російською мовою, вибачте. Однак зразки коду та підсумкова таблиця повинні бути зрозумілими.


1
Дякую за пояснення і +1 для вирішення, але ваша планована альтернатива з std::readlineі std::stoiне функціональні еквівалентна кодою OPS. Обидва cin >> x;і scanf("%f", &x);приймають пробіли мурашок як роздільник, в одному рядку може бути кілька чисел.
chqrlie

27

Проблема в тому, що, цитуючи cppreference :

будь-який вхід від std :: cin, вихід до std :: cerr або припинення програми змушує викликати std :: cout.flush ()

Це легко перевірити: якщо ви заміните

cin >> fact_num;

з

scanf("%d", &fact_num);

і однаковий для, cin >> num_of_inputsале зберігайте, coutви отримаєте майже таку ж продуктивність у вашій версії C ++ (або, швидше, версії IOStream), як і в одній C:

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

Те саме трапляється, якщо зберегти, cinале замінити

cout << num_of_trailing_zeros << "\n";

з

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

Просте рішення - розв’язати coutі, cinяк згадував Ілля Попов:

cin.tie(nullptr);

Реалізації стандартних бібліотек дозволяють опускати виклик змивання в певних випадках, але не завжди. Ось цитата з C ++ 14 27.7.2.1.3 (завдяки chqrlie):

Клас basic_istream :: sentry: По-перше, якщо is.tie () не є нульовим покажчиком, функція викликає is.tie () -> flush () для синхронізації вихідної послідовності з будь-яким пов'язаним зовнішнім потоком C. За винятком того, що цей виклик може бути придушений, якщо область розміщення is.tie () порожня. Далі реалізація дозволяє відкласти виклик на змив доти, поки не відбудеться виклик is.rdbuf () -> underflow (). Якщо такого виклику не відбувається до того, як об'єкт-охоронець буде знищений, виклик змивання може бути повністю усунутий.


Дякую за пояснення. Проте цитування C ++ 14 27.7.2.1.3: Клас basic_istream :: sentry : По-перше, якщо is.tie()це не нульовий покажчик, функція вимагає is.tie()->flush()синхронізації вихідної послідовності з будь-яким пов'язаним зовнішнім потоком C. За винятком того, що цей виклик може бути призупинено, якщо область is.tie()розміщення порожня. Далі реалізація дозволяє відкласти виклик на змив, поки не відбудеться виклик is.rdbuf()->underflow(). Якщо такого виклику не відбувається до того, як об'єкт-охоронець буде знищений, виклик змивання може бути повністю усунутий.
chqrlie

Як зазвичай у C ++, речі складніші, ніж виглядають. Бібліотека C ++ OP не настільки ефективна, наскільки дозволяє Стандарт.
chqrlie

Дякуємо за посилання cppreference. Мені не подобається "неправильна відповідь" на екрані друку ☺
Алекс Лоп.

@AlexLop. На жаль, виправлено проблему "неправильної відповіді" =). Забув оновити інший cin (хоча це не впливає на час).
vitaut

@chqrlie Правильно, але навіть у випадку недоливу продуктивність, швидше за все, буде гіршою порівняно з рішенням stdio. Дякуємо за стандартне посилання
vitaut
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.