Grande diferença (x9) no tempo de execução entre código quase idêntico em C e C ++

85

Estava a tentar resolver este exercício em www.spoj.com: FCTRL - Factorial

Você realmente não precisa ler, apenas faça se estiver curioso :)

Primeiro eu implementei em C ++ (aqui está minha solução):

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

Eu carreguei como a solução para g ++ 5.1

O resultado foi: Tempo 0,18 Mem 3,3M Resultados de execução C ++

Mas então eu vi alguns comentários que afirmavam que seu tempo de execução era inferior a 0,1. Como não conseguia pensar em um algoritmo mais rápido, tentei implementar o mesmo código em 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;
}

Eu carreguei como a solução para gcc 5.1

Desta vez, o resultado foi: Tempo 0,02 Mem 2,1M Resultados de execução C

Agora o código é quase o mesmo , adicionei std::ios_base::sync_with_stdio(false);ao código C ++ como foi sugerido aqui para desligar a sincronização com os buffers stdio da biblioteca C. Também divido o printf("%d\n", num_of_trailing_zeros);para printf("%d", num_of_trailing_zeros); printf("%s","\n");para compensar a dupla chamada de operator<<em cout << num_of_trailing_zeros << "\n";.

Mas eu ainda vi x9 melhor desempenho e menor uso de memória no código C vs. C ++.

Por que é que?

EDITAR

I fixo unsigned longpara unsigned into código C. Deveria ter sido unsigned inte os resultados mostrados acima estão relacionados à nova unsigned intversão ( ).

Alex Lop.
fonte
31
Os fluxos de C ++ são extremamente lentos por design. Porque devagar e sempre ganha a corrida. : P ( corre antes de ser inflamado )
Mysticial
7
A lentidão não vem de segurança ou adaptabilidade. É superdimensionado com todas as bandeiras de fluxo.
Karoly Horvath
8
@AlexLop. Usar a std::ostringstreampara acumular a saída e enviá-la a std::cout todas de uma vez no final reduz o tempo para 0,02. Usar std::coutem um loop é simplesmente mais lento em seu ambiente e não acho que haja uma maneira simples de melhorá-lo.
Blastfurnace
6
Ninguém mais está preocupado com o fato de que esses tempos foram obtidos usando ideone?
ildjarn
6
@Olaf: Lamento, mas discordo, esse tipo de pergunta é muito importante para todas as tags escolhidas. C e C ++ são suficientemente próximos em geral que tal diferença no desempenho implora por uma explicação. Estou feliz por termos encontrado. Talvez o GNU libc ++ deva ser melhorado como consequência.
chqrlie

Respostas:

56

Ambos os programas fazem exatamente a mesma coisa. Eles usam o mesmo algoritmo exato e, devido à sua baixa complexidade, seu desempenho está principalmente vinculado à eficiência do tratamento de entrada e saída.

digitalizar a entrada scanf("%d", &fact_num);de um lado e cin >> fact_num;do outro não parece muito caro de qualquer maneira. Na verdade, deve ser menos caro em C ++, pois o tipo de conversão é conhecido no momento da compilação e o analisador correto pode ser chamado diretamente pelo compilador C ++. O mesmo vale para a saída. Você até faz questão de escrever uma chamada separada para printf("%s","\n");, mas o compilador C é bom o suficiente para compilar isso como uma chamada para putchar('\n');.

Portanto, olhando para a complexidade de E / S e computação, a versão C ++ deve ser mais rápida que a versão C.

Desativar completamente o buffer de stdoutretarda a implementação C para algo ainda mais lento do que a versão C ++. Outro teste por AlexLop com um fflush(stdout);após o último printfproduz um desempenho semelhante ao da versão C ++. Não é tão lento quanto desabilitar completamente o buffer porque a saída é gravada no sistema em pequenos pedaços, em vez de um byte por vez.

Isso parece apontar para um comportamento específico em sua biblioteca C ++: eu suspeito da implementação de seu sistema cine coutlibera a saída para coutquando a entrada é solicitada de cin. Algumas bibliotecas C também fazem isso, mas geralmente apenas ao ler / gravar de e para o terminal. O benchmarking feito pelo site www.spoj.com provavelmente redireciona a entrada e a saída de e para os arquivos.

AlexLop fez outro teste: ler todas as entradas de uma vez em um vetor e, subsequentemente, computar e escrever todas as saídas ajuda a entender por que a versão C ++ é tão mais lenta. Ele aumenta o desempenho em relação à versão C, o que prova meu ponto e remove a suspeita sobre o código de formatação C ++.

Outro teste da Blastfurnace, armazenando todas as saídas em um std::ostringstreame liberando-as de uma vez no final, melhora o desempenho do C ++ em relação à versão C básica. QED.

Entrelaçar a entrada cine a saída coutparece causar um tratamento de E / S muito ineficiente, anulando o esquema de buffer de fluxo. reduzindo o desempenho por um fator de 10.

PS: seu algoritmo está incorreto fact_num >= UINT_MAX / 5porque fives *= 5irá estourar e quebrar antes de se tornar > fact_num. Você pode corrigir isso criando fivesum unsigned longou um unsigned long longse um desses tipos for maior que unsigned int. Use também %ucomo scanfformato. Você tem sorte de que os caras do www.spoj.com não sejam muito rígidos em seus benchmarks.

EDITAR: Como explicado posteriormente por vitaux, esse comportamento é realmente exigido pelo padrão C ++. cinestá vinculado coutpor padrão. Uma operação de entrada cinpara a qual o buffer de entrada precisa ser recarregado causará o coutesvaziamento da saída pendente. Na implementação do OP, cinparece fluir coutsistematicamente, o que é um pouco exagero e visivelmente ineficiente.

Ilya Popov forneceu uma solução simples para isso: cinpode ser desamarrado coutlançando outro feitiço mágico além de std::ios_base::sync_with_stdio(false);:

cin.tie(nullptr);

Observe também que tal descarga forçada também ocorre ao usar em std::endlvez de '\n'para produzir um fim de linha cout. Alterar a linha de saída para uma aparência mais idiomática e inocente do C ++ cout << num_of_trailing_zeros << endl;degradaria o desempenho da mesma forma.

chqrlie
fonte
2
Você provavelmente está certo sobre a descarga do jato. Coletar a saída em a std::ostringstreame enviá-la toda uma vez no final reduz o tempo para paridade com a versão C.
Blastfurnace
2
@ DavidC.Rankin: Arrisquei uma conjectura (cout fica vermelho ao ler cin), desenvolvi uma maneira de provar isso, AlexLop a implementou e dá evidências convincentes, mas Blastfurnace veio com uma maneira diferente de provar meu ponto e seus testes dar evidências igualmente convincentes. Eu considero isso uma prova, mas é claro que não é uma prova completamente formal, olhando para o código-fonte C ++ poderia.
chqrlie
2
Tentei usar ostringstreampara a saída e deu tempo 0,02 QED :). Em relação ao fact_num >= UINT_MAX / 5, bom ponto!
Alex Lop.
1
Coletar todas as entradas em a vectore depois processar os cálculos (sem ostringstream) dá o mesmo resultado! Tempo 0,02. Combinando vectore ostringstreamnão melhorá-lo mais. Mesmo tempo 0,02
Alex Lop.
2
Uma correção mais simples que funciona mesmo se sizeof(int) == sizeof(long long)for esta: adicione um teste no corpo do loop depois num_of_trailing_zeros += fact_num/fives;para verificar se fivesatingiu seu máximo:if (fives > UINT_MAX / 5) break;
chqrlie
44

Outro truque para tornar iostreams mais rápido quando você usa ambos cine couté chamar

cin.tie(nullptr);

Por padrão, quando você insere qualquer coisa de cin, ele esvazia cout. Isso pode prejudicar significativamente o desempenho se você fizer entradas e saídas intercaladas. Isso é feito para os usos da interface de linha de comando, onde você mostra algum prompt e espera pelos dados:

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

Neste caso, você deseja ter certeza de que o prompt é realmente mostrado antes de começar a esperar pela entrada. Com a linha acima, você quebra esse laço cine coutse torna independente.

Desde C ++ 11, mais uma maneira de obter melhor desempenho com iostreams é usar std::getlinejunto com std::stoi, desta forma:

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

Dessa forma, pode chegar perto do estilo C em desempenho, ou até mesmo superar scanf. Usar getchare especialmente getchar_unlockedjunto com a análise manuscrita ainda fornece um melhor desempenho.

PS. Eu escrevi um post comparando várias maneiras de inserir números em C ++, útil para juízes online, mas é apenas em russo, desculpe. Os exemplos de código e a tabela final, no entanto, devem ser compreensíveis.

Ilya Popov
fonte
1
Obrigado pela explicação e +1 para a solução, mas sua alternativa proposta com std::readlinee std::stoinão é funcionalmente equivalente ao código OPs. Ambos cin >> x;e scanf("%f", &x);aceitam espaços em branco como separadores, pode haver vários números na mesma linha.
chqrlie
27

O problema é que, citando cppreference :

qualquer entrada de std :: cin, saída para std :: cerr ou encerramento do programa força uma chamada para std :: cout.flush ()

Isso é fácil de testar: se você substituir

cin >> fact_num;

com

scanf("%d", &fact_num);

e o mesmo para, cin >> num_of_inputsmas manter, coutvocê obterá praticamente o mesmo desempenho em sua versão C ++ (ou melhor, versão IOStream) como em C um:

insira a descrição da imagem aqui

O mesmo acontece se você mantiver, cinmas substituir

cout << num_of_trailing_zeros << "\n";

com

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

Uma solução simples é desamarrar coute cinconforme mencionado por Ilya Popov:

cin.tie(nullptr);

As implementações da biblioteca padrão podem omitir a chamada para flush em certos casos, mas nem sempre. Aqui está uma citação de C ++ 14 27.7.2.1.3 (graças a chqrlie):

Classe basic_istream :: sentry: Primeiro, se is.tie () não for um ponteiro nulo, a função chama is.tie () -> flush () para sincronizar a sequência de saída com qualquer fluxo C externo associado. Exceto que essa chamada pode ser suprimida se a área put de is.tie () estiver vazia. Além disso, uma implementação é permitida para adiar a chamada para flush até que uma chamada de is.rdbuf () -> underflow () ocorra. Se nenhuma chamada ocorrer antes que o objeto sentinela seja destruído, a chamada para flush pode ser totalmente eliminada.

vitaut
fonte
Obrigada pelo esclarecimento. Ainda citando C ++ 14 27.7.2.1.3: Classe basic_istream :: sentry : Primeiro, se is.tie()não for um ponteiro nulo, a função chama is.tie()->flush()para sincronizar a sequência de saída com qualquer fluxo C externo associado. Exceto que essa chamada pode ser suprimida se a área put de is.tie()estiver vazia. Além disso, uma implementação é permitida para adiar a chamada para liberar até que is.rdbuf()->underflow()ocorra uma chamada de . Se nenhuma chamada ocorrer antes que o objeto sentinela seja destruído, a chamada para flush pode ser totalmente eliminada.
chqrlie
Como de costume com C ++, as coisas são mais complexas do que parecem. A biblioteca C ++ do OP não é tão eficiente quanto o padrão permite.
chqrlie
Obrigado pelo link cppreference. Eu não gosto da "resposta errada" na tela de impressão ☺
Alex Lop.
@AlexLop. Ops, corrigido o problema de "resposta errada" =). Esqueci de atualizar a outra cin (isso não afeta o tempo).
vitaut
@chqrlie Certo, mas mesmo no caso de underflow o desempenho provavelmente será pior em comparação com a solução stdio. Obrigado pela referência padrão.
vitaut