Por que você deseja evitar a descarga de stdout?

Me deparei com uma pergunta em Codereview e em uma resposta o feedback foi evite std::endl porque ele libera o fluxo. A citação completa é:

Aconselho a evitar std::endl em geral. Além de escrever um novo linha para o fluxo, ele libera o fluxo. Você quer a nova linha, mas quase nunca quer limpar o fluxo, então geralmente é melhor apenas escrever um \ n. Nas raras ocasiões em que você realmente quiser o flush, faça-o explicitamente: std::cout << "\n" << std::flush;.

O postador fez não explico isso, nem no post ou nos comentários. Portanto, minha pergunta é simplesmente esta:

Por que você deseja evitar a descarga?

O que me deixou ainda mais curioso foi que o pôster diz que é muito raro você querer dar descarga. Não tenho problemas em imaginar situações em que você queira evitar a descarga, mas ainda pensei que, em geral, você gostaria de flush quando você imprime uma nova linha. Afinal, não é essa a razão pela qual std::endl está flushing em primeiro lugar?

Apenas para comentar os votos de fechamento com antecedência:

Não considero esta opinião baseada. O que você deve preferir pode ser baseado em opinião, mas há razões objetivas a levar em consideração. As respostas até agora provam isso. A descarga afeta o desempenho.

Comentários

Resposta

A resposta curta e simples é que o uso de std::endl pode e irá diminuir a produção por uma margem enorme. Na verdade, estou razoavelmente convencido de que std::endl é responsável pela maior parte da noção de que iostreams C ++ são substancialmente mais lentos do que I / O de estilo C.

Por exemplo, considere um programa como este:

#include <iostream> #include <string> #include <sstream> #include <time.h> #include <iomanip> #include <algorithm> #include <iterator> #include <stdio.h> char fmt[] = "%s\n"; static const int count = 3000000; static char const *const string = "This is a string."; static std::string s = std::string(string) + "\n"; void show_time(void (*f)(), char const *caption) { clock_t start = clock(); f(); clock_t ticks = clock()-start; std::cerr << std::setw(30) << caption << ": " << (double)ticks/CLOCKS_PER_SEC << "\n"; } void use_printf() { for (int i=0; i<count; i++) printf(fmt, string); } void use_puts() { for (int i=0; i<count; i++) puts(string); } void use_cout() { for (int i=0; i<count; i++) std::cout << string << "\n"; } void use_cout_unsync() { std::cout.sync_with_stdio(false); for (int i=0; i<count; i++) std::cout << string << "\n"; std::cout.sync_with_stdio(true); } void use_stringstream() { std::stringstream temp; for (int i=0; i<count; i++) temp << string << "\n"; std::cout << temp.str(); } void use_endl() { for (int i=0; i<count; i++) std::cout << string << std::endl; } void use_fill_n() { std::fill_n(std::ostream_iterator<char const *>(std::cout, "\n"), count, string); } void use_write() { for (int i = 0; i < count; i++) std::cout.write(s.data(), s.size()); } int main() { show_time(use_printf, "Time using printf"); show_time(use_puts, "Time using puts"); show_time(use_cout, "Time using cout (synced)"); show_time(use_cout_unsync, "Time using cout (un-synced)"); show_time(use_stringstream, "Time using stringstream"); show_time(use_endl, "Time using endl"); show_time(use_fill_n, "Time using fill_n"); show_time(use_write, "Time using write"); return 0; } 

Com a saída padrão redirecionada para um arquivo, isso produz os seguintes resultados:

 Time using printf: 0.208539 Time using puts: 0.103065 Time using cout (synced): 0.241377 Time using cout (un-synced): 0.181853 Time using stringstream: 0.223617 Time using endl: 4.32881 Time using fill_n: 0.209951 Time using write: 0.102781 

Usar std::endl desacelerou o programa em um fator de cerca de 20 neste caso. Se você escreveu strings mais curtas, o desacelerar poderia / seria ainda maior.

Existem alguns casos em que você realmente deseja liberar um stream manualmente – mas, honestamente, são poucos e distantes entre si.

Na maioria das vezes, um stream precisa ser liberado (por exemplo, você imprime um prompt e espera por alguma entrada), ele “acontecerá automaticamente, a menos que você” tenha usado coisas como std::tie e / ou std::sync_with_stdio para evitar isso.

Isso deixa apenas um pequeno número de situações verdadeiramente incomuns em que você um bom motivo para liberar um fluxo manualmente. Esses casos são raros o suficiente para que valha a pena usar std::flush quando acontecerem, para tornar aparente para quem está lendo o código que você está liberando o fluxo intencionalmente (e com mais frequência do que não, provavelmente também merece um comentário sobre por que este é um dos raros casos em que a descarga do fluxo realmente faz sentido).

Resposta

Cada vez que um processo produz uma saída, ele precisa chamar uma função que realmente faz o trabalho. Na maioria dos casos, essa função é basicamente write(2). Em um sistema operacional multitarefa, a chamada para write() fará uma armadilha no kernel, que deve interromper o processo, lidar com o I / O, fazer outras coisas enquanto quaisquer bloqueios são eliminados, coloque na fila de espera e colocá-lo em execução novamente quando chegar a hora. Coletivamente, você pode chamar toda essa atividade de sobrecarga de chamada do sistema . Se isso parece muito, é.

Limpar um fluxo em buffer * depois de gravar uma pequena quantidade de dados ou não ter nenhum buffer provoca uma sobrecarga cada vez que você faz isso:

 1\n (System call that writes two bytes) 2\n (System call that writes two bytes) 3\n (System call that writes two bytes) 4\n (System call that writes two bytes) 5\n (System call that writes two bytes)  

Era assim que era feito nos primeiros dias, até que alguém descobriu que estava queimando um muito tempo do sistema. A sobrecarga poderia ser mantida baixa acumulando a saída em um buffer até que estivesse cheia ou até que o programa decidisse que ela deveria ser enviada imediatamente.(Você pode querer fazer o último se estiver produzindo uma saída esporadicamente que precisa ser vista ou consumida.) Evitar uma descarga no final de cada linha diminui o número de chamadas do sistema e a sobrecarga incorrida:

 1\n 2\n 3\n 4\n 5\n (Flush) (System call that writes ten bytes)  

* Observe que o conceito de saída padrão é um descritor de arquivo associado a um processo e dado um número bem conhecido. Isso difere do stdout definido por C, C ++ e outros, que são identificadores para implementações de um fluxo em buffer que vive inteiramente no usuário e grava no saída padrão. A write() chamada do sistema não é armazenada em buffer.

Comentários

  • Se e quando liberar depende do contexto em que a saída será usada. Um programa orientado para a taxa de transferência só deve ser liberado quando seus buffers estiverem cheios. Um programa sensível à latência deve ser liberado com mais frequência. Por exemplo, se a saída for para um console, você ainda Vou liberar após cada nova linha. Programas interativos (que mostram algum prompt de entrada) devem esvaziar imediatamente, mesmo se a linha ainda não estiver cheia.
  • ” se a saída for para um console, você ainda seria liberado após cada nova linha. ” Verdadeiro, mas se a saída for para um console, haverá uma liberação automática após cada nova linha. Não há necessidade de fazer isso explicitamente.
  • @amon Você ainda gostaria de unir a saída consecutiva, pois a liberação oportuna, ou seja, antes de solicitar a entrada e sem atraso, é suficiente. Reconhecidamente, é melhor liberar uma vez com muita frequência do que atrasar muito em mostrar o resultado ou investir muito trabalho na otimização …
  • Um truque às vezes usado pelos bufferers é reiniciar um cronômetro quando novas coisas chegarem e libere quando o cronômetro acabar.

Resposta

Por que o flush deve ser evitado:

Porque o IO funciona melhor quando o sistema operacional pode trabalhar com quantidades relativamente grandes de dados. Flushes regulares com pequenas quantidades de dados causam lentidão, às vezes de forma muito significativa.

Por que você quase nunca deve limpar manualmente:

Existem liberações automáticas que cobrem a maioria dos casos de uso. Por exemplo, se um programa grava no console, o sistema, por padrão, é liberado após cada nova linha. Ou se você gravar em um arquivo, os dados serão gravados assim que houver dados suficientes para serem gravados de uma vez, e também quando o arquivo for fechado.

Quando você deve descarregar manualmente:

Se você explicitamente precisar atualizar a saída imediatamente. Exemplo: se você criar um controle giratório ou barra de progresso que substitui repetidamente a linha atual. Ou se você der saída em arquivo e realmente quiser que o arquivo seja atualizado em momentos específicos.

Comentários

  • O sistema não pode liberar nenhum buffer de usuário. Ou você inclui bibliotecas, especialmente a biblioteca padrão, também no ” sistema “? Claro, stdin e stdout são geralmente acoplados, pelo menos, se ambos usarem o mesmo console.
  • Sim, Não ‘ achei que não seria uma boa ideia entrar em muitos detalhes sobre como as coisas são divididas entre as bibliotecas do espaço do usuário e o espaço do kernel.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *