Grande differenza (x9) nel tempo di esecuzione tra codice quasi identico in C e C ++


85

Stavo cercando di risolvere questo esercizio da www.spoj.com: FCTRL - Factorial

Non devi davvero leggerlo, fallo solo se sei curioso :)

Per prima cosa l'ho implementato in C ++ (ecco la mia soluzione):

#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;
}

L'ho caricato come soluzione per g ++ 5.1

Il risultato è stato: Time 0.18 Mem 3.3M Risultati dell'esecuzione C ++

Ma poi ho visto alcuni commenti che affermavano che il loro tempo di esecuzione era inferiore a 0,1. Dato che non riuscivo a pensare a un algoritmo più veloce, ho provato a implementare lo stesso codice in 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;
}

L'ho caricato come soluzione per gcc 5.1

Questa volta il risultato è stato: Time 0.02 Mem 2.1M Risultati dell'esecuzione C.

Ora il codice è quasi lo stesso , ho aggiunto std::ios_base::sync_with_stdio(false);al codice C ++ come suggerito qui per disattivare la sincronizzazione con i buffer stdio della libreria C. Ho anche diviso il printf("%d\n", num_of_trailing_zeros);a printf("%d", num_of_trailing_zeros); printf("%s","\n");per compensare la doppia chiamata di operator<<incout << num_of_trailing_zeros << "\n"; .

Ma ho ancora visto prestazioni migliori x9 e un minore utilizzo della memoria nel codice C rispetto a C ++.

Perché?

MODIFICARE

Ho fissato unsigned longper unsigned intil codice C. Avrebbe dovuto essere unsigned intei risultati mostrati sopra sono relativi alla nuova unsigned intversione ( ).


31
I flussi C ++ sono estremamente lenti per progettazione. Perché lento e costante vince la gara. : P ( corre prima che mi
prenda

7
La lentezza non viene dalla sicurezza o dall'adattabilità. È troppo progettato con tutte le bandiere del flusso.
Karoly Horvath

8
@AlexLop. Usare a std::ostringstreamper accumulare l'output e inviarlo a std::cout tutti in una volta alla fine porta il tempo a 0,02. L'uso std::coutin un ciclo è semplicemente più lento nel loro ambiente e non credo che ci sia un modo semplice per migliorarlo.
Blastfurnace

6
Nessun altro è preoccupato dal fatto che questi tempi siano stati ottenuti utilizzando ideone?
ildjarn

6
@Olaf: Temo di non essere d'accordo, questo tipo di domanda è molto in tema per tutti i tag scelti. C e C ++ sono sufficientemente simili in generale che una tale differenza di prestazioni richiede una spiegazione. Sono contento di averlo trovato. Forse la GNU libc ++ dovrebbe essere migliorata di conseguenza.
chqrlie

Risposte:


56

Entrambi i programmi fanno esattamente la stessa cosa. Usano lo stesso algoritmo esatto e, data la sua bassa complessità, le loro prestazioni sono per lo più legate all'efficienza della gestione di input e output.

scansionare l'ingresso con scanf("%d", &fact_num);da un lato e cin >> fact_num;dall'altro non sembra molto costoso in entrambi i casi. In effetti dovrebbe essere meno costoso in C ++ poiché il tipo di conversione è noto in fase di compilazione e il parser corretto può essere richiamato direttamente dal compilatore C ++. Lo stesso vale per l'output. È anche importante scrivere una chiamata separata per printf("%s","\n");, ma il compilatore C è abbastanza buono da compilarlo come una chiamata a putchar('\n');.

Quindi, guardando la complessità sia dell'I / O che del calcolo, la versione C ++ dovrebbe essere più veloce della versione C.

Disabilitare completamente il buffering di stdoutrallenta l'implementazione del C a qualcosa di ancora più lento della versione C ++. Un altro test di AlexLop con un fflush(stdout);dopo l'ultimo printfproduce prestazioni simili a quelle della versione C ++. Non è così lento come disabilitare completamente il buffering perché l'output viene scritto nel sistema in piccoli blocchi invece di un byte alla volta.

Questo sembra indicare un comportamento specifico nella tua libreria C ++: sospetto l'implementazione del tuo sistema cine coutscarica l'output coutquando viene richiesto l'input cin. Anche alcune librerie C lo fanno, ma di solito solo durante la lettura / scrittura da e verso il terminale. Il benchmarking eseguito dal sito www.spoj.com probabilmente reindirizza input e output da e verso i file.

AlexLop ha fatto un altro test: leggere tutti gli input contemporaneamente in un vettore e successivamente calcolare e scrivere tutto l'output aiuta a capire perché la versione C ++ è molto più lenta. Aumenta le prestazioni rispetto a quelle della versione C, questo dimostra il mio punto e rimuove i sospetti sul codice di formattazione C ++.

Un altro test di Blastfurnace, memorizzando tutti gli output in un std::ostringstreame scaricando che in un colpo solo alla fine, migliora le prestazioni del C ++ rispetto a quelle della versione C di base. QED.

L'interlacciamento dell'input cine dell'output a coutsembra causare una gestione I / O molto inefficiente, annullando lo schema di buffering del flusso. riducendo le prestazioni di un fattore 10.

PS: il tuo algoritmo non è corretto fact_num >= UINT_MAX / 5perché fives *= 5traboccerà e si avvolgerà prima che lo diventi > fact_num. Puoi correggerlo creando fivesun unsigned longo un unsigned long longse uno di questi tipi è maggiore di unsigned int. Usa anche %ucome scanfformato. Sei fortunato che i ragazzi di www.spoj.com non siano troppo severi nei loro benchmark.

EDIT: come spiegato in seguito da vitaux, questo comportamento è effettivamente imposto dallo standard C ++. cinè legato a coutper impostazione predefinita. Un'operazione di input da cincui il buffer di input deve essere riempito causerà coutlo svuotamento dell'output in sospeso. Nell'implementazione del PO, cinsembra svuotare coutsistematicamente, il che è un po 'eccessivo e visibilmente inefficiente.

Ilya Popov ha fornito una soluzione semplice per questo: cinpuò essere sciolto coutlanciando un altro incantesimo magico oltre a std::ios_base::sync_with_stdio(false);:

cin.tie(nullptr);

Notare inoltre che tale svuotamento forzato si verifica anche quando si usa std::endlinvece di '\n'produrre un fine riga cout. Cambiare la riga di output in un aspetto più idiomatico e innocente del C ++ peggiorerebbe le cout << num_of_trailing_zeros << endl;prestazioni allo stesso modo.


2
Probabilmente hai ragione sullo stream flush. Raccogliere l'output in a std::ostringstreamed emetterlo tutto una volta alla fine porta il tempo alla pari con la versione C.
Blastfurnace

2
@ DavidC.Rankin: ho azzardato una congettura (cout è arrossato leggendo cin), ha escogitato un modo per dimostrarlo, AlexLop lo ha implementato e fornisce prove convincenti, ma Blastfurnace ha escogitato un modo diverso per dimostrare il mio punto e le sue prove dare prove altrettanto convincenti. Lo prendo per una prova, ma ovviamente non è una prova completamente formale, guardando il codice sorgente C ++ potrebbe.
chqrlie

2
Ho provato a usare ostringstreamper l'output e ha dato Time 0.02 QED :). Per quanto riguarda il fact_num >= UINT_MAX / 5, BUON punto!
Alex Lop.

1
Raccogliendo tutti gli input in a vectore quindi elaborando i calcoli (senza ostringstream) si ottiene lo stesso risultato! Tempo 0,02. Combinando entrambi vectore ostringstreamnon lo migliora di più. Stesso tempo 0,02
Alex Lop.

2
Una soluzione più semplice che funziona anche se sizeof(int) == sizeof(long long)è questa: aggiungi un test nel corpo del ciclo dopo num_of_trailing_zeros += fact_num/fives;per verificare se fivesha raggiunto il suo massimo:if (fives > UINT_MAX / 5) break;
chqrlie

44

Un altro trucco per rendere iostreams più veloci quando usi entrambi cine coutè chiamare

cin.tie(nullptr);

Per impostazione predefinita, quando inserisci qualcosa da cin, viene scaricato cout. Può danneggiare in modo significativo le prestazioni se si utilizzano input e output interleaved. Questo viene fatto per gli usi dell'interfaccia della riga di comando, dove mostri un prompt e poi attendi i dati:

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

In questo caso, assicurati che il prompt sia effettivamente visualizzato prima di iniziare ad attendere l'input. Con la linea sopra rompi quel legame cine coutdiventi indipendente.

A partire da C ++ 11, un altro modo per ottenere prestazioni migliori con iostream è usare std::getlineinsieme a std::stoi, in questo modo:

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

In questo modo può avvicinarsi allo stile C nelle prestazioni, o addirittura superare scanf. Usando getchare soprattuttogetchar_unlocked insieme all'analisi scritta a mano fornisce ancora prestazioni migliori.

PS. Ho scritto un post confrontando diversi modi per inserire numeri in C ++, utile per i giudici online, ma è solo in russo, scusate. Gli esempi di codice e il tavolo finale, tuttavia, dovrebbero essere comprensibili.


1
Grazie per la spiegazione e +1 per la soluzione, ma la tua alternativa proposta con std::readlinee std::stoinon è funzionalmente equivalente al codice OP. Entrambi cin >> x;e scanf("%f", &x);accettano gli spazi bianchi delle formiche come separatori, possono esserci più numeri sulla stessa riga.
chqrlie

27

Il problema è che, citando cppreference :

qualsiasi input da std :: cin, output a std :: cerr o la chiusura del programma forza una chiamata a std :: cout.flush ()

Questo è facile da testare: se sostituisci

cin >> fact_num;

con

scanf("%d", &fact_num);

e lo stesso per, cin >> num_of_inputsma mantieni coutle stesse prestazioni nella tua versione C ++ (o, piuttosto, versione IOStream) come in quella C:

inserisci qui la descrizione dell'immagine

Lo stesso accade se mantieni cinma sostituisci

cout << num_of_trailing_zeros << "\n";

con

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

Una soluzione semplice è sciogliere coute cincome menzionato da Ilya Popov:

cin.tie(nullptr);

Le implementazioni della libreria standard possono omettere la chiamata a flush in alcuni casi, ma non sempre. Ecco una citazione da C ++ 14 27.7.2.1.3 (grazie a chqrlie):

Classe basic_istream :: sentry: Primo, se is.tie () non è un puntatore nullo, la funzione chiama is.tie () -> flush () per sincronizzare la sequenza di output con qualsiasi flusso C esterno associato. Tranne che questa chiamata può essere soppressa se l'area put di is.tie () è vuota. Inoltre, un'implementazione può posticipare la chiamata a flush fino a quando non si verifica una chiamata di is.rdbuf () -> underflow (). Se nessuna chiamata di questo tipo si verifica prima che l'oggetto sentinella venga distrutto, la chiamata a flush può essere eliminata completamente.


Grazie per la spiegazione. Citando ancora C ++ 14 27.7.2.1.3: Class basic_istream :: sentry : Primo, se is.tie()non è un puntatore nullo, la funzione chiama is.tie()->flush()per sincronizzare la sequenza di output con qualsiasi flusso C esterno associato. Tranne che questa chiamata può essere soppressa se l'area di inserimento di is.tie()è vuota. Inoltre, un'implementazione è consentita per rinviare la chiamata a flush fino a quando non si is.rdbuf()->underflow()verifica una chiamata di . Se nessuna chiamata di questo tipo si verifica prima che l'oggetto sentinella venga distrutto, la chiamata a flush può essere eliminata completamente.
chqrlie

Come al solito con C ++, le cose sono più complesse di quanto sembrano. La libreria C ++ dell'OP non è efficiente come lo Standard consente di essere.
chqrlie

Grazie per il link cppreference. Tuttavia non mi piace la "risposta sbagliata" nella schermata di stampa ☺
Alex Lop.

@AlexLop. Oops, risolto il problema della "risposta sbagliata" =). Ho dimenticato di aggiornare l'altro cin (questo non influisce sui tempi però).
Vitaut

@chqrlie Giusto, ma anche nel caso di underflow è probabile che le prestazioni siano peggiori rispetto alla soluzione stdio. Grazie per lo standard rif.
vitaut
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.