Quão ruim é chamar println () frequentemente do que concatenar seqüências de caracteres juntas e chamá-lo uma vez?

23

Eu sei que a saída para o console é uma operação cara. No interesse da legibilidade do código, às vezes, é bom chamar uma função para gerar texto duas vezes, em vez de ter uma longa sequência de texto como argumento.

Por exemplo, quanto menos eficiente é ter

System.out.println("Good morning.");
System.out.println("Please enter your name");

vs.

System.out.println("Good morning.\nPlease enter your name");

No exemplo, a diferença é apenas uma chamada, println()mas e se for mais?

Em uma nota relacionada, as instruções que envolvem a impressão de texto podem parecer estranhas ao exibir o código-fonte se o texto a ser impresso for longo. Supondo que o texto em si não possa ser reduzido, o que pode ser feito? Deve ser este o caso em que várias println()chamadas são feitas? Alguém me disse uma vez que uma linha de código não deveria ter mais de 80 caracteres (IIRC), então o que você faria com

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

O mesmo vale para idiomas como C / C ++, pois cada vez que os dados são gravados em um fluxo de saída, uma chamada do sistema deve ser feita e o processo deve ir para o modo kernel (o que é muito caro)?

Celeritas
fonte
Mesmo que esse seja um código muito pequeno, devo dizer que estive pensando a mesma coisa. Seria bom determinar a resposta para isso de uma vez por todas
Simon Forsberg
@ SimonAndréForsberg Não tenho certeza se é aplicável ao Java porque é executado em uma máquina virtual, mas em linguagens de nível inferior, como C / C ++, eu imaginaria que seria caro cada vez que algo é gravado em um fluxo de saída, uma chamada de sistema deve ser feito.
Há essa considerar também: stackoverflow.com/questions/21947452/...
HJK
1
Eu tenho que dizer que não vejo o ponto aqui. Ao interagir com um usuário via terminal, não consigo imaginar problemas de desempenho, porque geralmente não há muito para imprimir. E aplicativos com uma GUI ou um aplicativo da web devem gravar em um arquivo de log (geralmente usando uma estrutura).
Andy
1
Se você está dizendo bom dia, faz isso uma ou duas vezes por dia. Otimização não é uma preocupação. Se for algo mais, você precisa criar um perfil para saber se é um problema. O código em que trabalho no log torna o código inutilizável, a menos que você crie um buffer de várias linhas e despeje o texto em uma chamada.
18718 Mattnz #

Respostas:

29

Existem duas 'forças' aqui, em tensão: desempenho versus legibilidade.

Vamos abordar o terceiro problema primeiro, longas filas:

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

A melhor maneira de implementar isso e manter a legibilidade é usar a concatenação de strings:

System.out.println("Good morning everyone. I am here today to present you "
                 + "with a very, very lengthy sentence in order to prove a "
                 + "point about how it looks strange amongst other code.");

A concatenação constante de String ocorrerá no tempo de compilação e não terá efeito sobre o desempenho. As linhas são legíveis e você pode simplesmente seguir em frente.

Agora, sobre o:

System.out.println("Good morning.");
System.out.println("Please enter your name");

vs.

System.out.println("Good morning.\nPlease enter your name");

A segunda opção é significativamente mais rápida. Vou sugerir cerca de 2X o mais rápido .... por quê?

Porque 90% (com uma ampla margem de erro) do trabalho não está relacionado ao descarte dos caracteres na saída, mas é uma sobrecarga necessária para garantir a saída da gravação.

Sincronização

System.outé um PrintStream. Todas as implementações de Java que eu conheço sincronizam internamente o PrintStream: Veja o código no GrepCode! .

O que isso significa para o seu código?

Isso significa que, toda vez que você liga System.out.println(...)para sincronizar seu modelo de memória, você está verificando e aguardando um bloqueio. Quaisquer outros segmentos que chamam System.out também serão bloqueados.

Em aplicativos de thread único, o impacto System.out.println()geralmente é limitado pelo desempenho de E / S do seu sistema, com que rapidez você pode gravar em um arquivo. Em aplicativos multithread, o bloqueio pode ser mais um problema do que o IO.

Flushing

Cada impressão é liberada . Isso faz com que os buffers sejam limpos e aciona uma gravação no nível do console nos buffers. A quantidade de esforço realizado aqui depende da implementação, mas, geralmente, entende-se que o desempenho da liberação está apenas em pequena parte relacionado ao tamanho do buffer que está sendo liberado. Há uma sobrecarga significativa relacionada à liberação, onde os buffers de memória são marcados como sujos, a máquina virtual está executando E / S e assim por diante. Incorrer nessa sobrecarga uma vez, em vez de duas, é uma otimização óbvia.

Alguns números

Eu montei o seguinte pequeno teste:

public class ConsolePerf {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            benchmark("Warm " + i);
        }
        benchmark("real");
    }

    private static void benchmark(String string) {
        benchString(string + "short", "This is a short String");
        benchString(string + "long", "This is a long String with a number of newlines\n"
                  + "in it, that should simulate\n"
                  + "printing some long sentences and log\n"
                  + "messages.");

    }

    private static final int REPS = 1000;

    private static void benchString(String name, String value) {
        long time = System.nanoTime();
        for (int i = 0; i < REPS; i++) {
            System.out.println(value);
        }
        double ms = (System.nanoTime() - time) / 1000000.0;
        System.err.printf("%s run in%n    %12.3fms%n    %12.3f lines per ms%n    %12.3f chars per ms%n",
                name, ms, REPS/ms, REPS * (value.length() + 1) / ms);

    }


}

O código é relativamente simples, imprime repetidamente uma cadeia curta ou longa para a saída. A cadeia longa possui várias linhas novas. Ele mede quanto tempo leva para imprimir 1000 iterações de cada.

Se eu executá-lo no prompt de comando do unix (Linux), redirecionar o STDOUTpara /dev/nulle imprimir os resultados reais STDERR, posso fazer o seguinte:

java -cp . ConsolePerf > /dev/null 2> ../errlog

A saída (no errlog) é semelhante a:

Warm 0short run in
           7.264ms
         137.667 lines per ms
        3166.345 chars per ms
Warm 0long run in
           1.661ms
         602.051 lines per ms
       74654.317 chars per ms
Warm 1short run in
           1.615ms
         619.327 lines per ms
       14244.511 chars per ms
Warm 1long run in
           2.524ms
         396.238 lines per ms
       49133.487 chars per ms
.......
Warm 99short run in
           1.159ms
         862.569 lines per ms
       19839.079 chars per ms
Warm 99long run in
           1.213ms
         824.393 lines per ms
      102224.706 chars per ms
realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

O que isto significa? Deixe-me repetir a última estrofe:

realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Isso significa que, para todos os efeitos, mesmo que a linha 'longa' seja cerca de cinco vezes maior e contenha várias linhas novas, leva apenas o tempo necessário para produzir a linha curta.

O número de caracteres por segundo a longo prazo é 5 vezes maior e o tempo decorrido é aproximadamente o mesmo ...

Em outras palavras, seu desempenho é escalável em relação ao número de impressoras que você possui, não ao que elas imprimem.

Atualização: o que acontece se você redirecionar para um arquivo, em vez de para / dev / null?

realshort run in
           2.592ms
         385.815 lines per ms
        8873.755 chars per ms
reallong run in
           2.686ms
         372.306 lines per ms
       46165.955 chars per ms

É muito mais lento, mas as proporções são as mesmas ....

rolfl
fonte
Adicionado alguns números de desempenho.
Rolfl 01/07/19
Você também deve considerar o problema que "\n"pode não ser o terminador de linha correto. printlnterminará automaticamente a linha com o (s) caractere (s) certo (s), mas a inserção \ndireta de uma string pode causar problemas. Se você deseja fazer o que é certo, pode ser necessário usar a formatação de sequência ou a line.separatorpropriedade do sistema . printlné muito mais limpo.
User2357112 suporta Monica
3
Tudo isso é uma ótima análise, portanto, com +1, com certeza, mas eu diria que, uma vez que você esteja comprometido com a saída do console, essas pequenas diferenças de desempenho desaparecerão pela janela. Se o algoritmo do seu programa for mais rápido que a saída dos resultados (nesse pequeno nível de saída), você poderá imprimir cada caractere um por um e não perceber a diferença.
David Harkness
Acredito que seja uma diferença entre Java e C / C ++ que a saída esteja sincronizada. Digo isso porque lembro de escrever um programa multithread e ter problemas com a saída ilegível se diferentes threads tentarem gravar para gravar no console. Alguém pode verificar isso?
6
É importante lembrar também que nada dessa velocidade importa quando colocado ao lado da função que aguarda a entrada do usuário.
Vmrob
2

Eu não acho que ter um monte de printlns seja um problema de design. A meu ver, isso pode ser feito claramente com o analisador de código estático, se é realmente um problema.

Mas isso não é um problema, porque a maioria das pessoas não faz IOs assim. Quando eles realmente precisam realizar muitas IOs, eles usam os armazenados em buffer (BufferedReader, BufferedWriter, etc.) quando a entrada é armazenada em buffer, você verá que o desempenho é semelhante o suficiente, e não precisa se preocupar em ter um monte de printlnou poucos println.

Então, para responder à pergunta original. Eu diria que nada mal se você printlnimprimir algumas coisas como a maioria das pessoas usaria println.

InformedA
fonte
1

Em linguagens de nível superior, como C e C ++, isso é menos problemático do que em Java.

Em primeiro lugar, C e C ++ definem a concatenação de strings em tempo de compilação, para que você possa algo como:

std::cout << "Good morning everyone. I am here today to present you with a very, "
    "very lengthy sentence in order to prove a point about how it looks strange "
    "amongst other code.";

Nesse caso, concatenar a string não é apenas uma otimização que você pode usar, geralmente (etc.) depende do compilador a fazer. Em vez disso, é diretamente exigido pelos padrões C e C ++ (fase 6 da tradução: "Tokens literais de cadeias adjacentes são concatenados.").

Embora isso ocorra à custa de um pouco de complexidade extra no compilador e na implementação, C e C ++ fazem um pouco mais para ocultar a complexidade da produção eficiente de saída do programador. Java é muito mais parecido com a linguagem assembly - cada chamada se System.out.printlntraduz muito mais diretamente em uma chamada para a operação subjacente para gravar os dados no console. Se você deseja armazenar em buffer para melhorar a eficiência, isso deve ser fornecido separadamente.

Isso significa, por exemplo, que em C ++, reescrevendo o exemplo anterior, para algo assim:

std::cout << "Good morning everyone. I am here today to present you with a very, ";
std::cout << "very lengthy sentence in order to prove a point about how it looks ";       
std::cout << "strange amongst other code.";

... normalmente 1 tem quase nenhum efeito sobre a eficiência. Cada uso de coutsimplesmente depositaria dados em um buffer. Esse buffer seria liberado para o fluxo subjacente quando o buffer fosse preenchido ou o código tentasse ler a entrada do uso (como com std::cin).

iostreams também possuem uma sync_with_stdiopropriedade que determina se a saída do iostreams é sincronizada com a entrada no estilo C (por exemplo, getchar). Por padrão, sync_with_stdioé definido como true; portanto, se, por exemplo, você gravar em std::cout, e ler por getchar, os dados para os quais você gravou coutserão liberados quando getcharchamados. Você pode definir sync_with_stdiocomo false para desativar isso (geralmente feito para melhorar o desempenho).

sync_with_stdiotambém controla um grau de sincronização entre os threads. Se a sincronização estiver ativada (o padrão), a gravação em um iostream a partir de vários encadeamentos poderá resultar na intercalação dos dados dos encadeamentos, mas impedirá quaisquer condições de corrida. IOW, seu programa executará e produzirá saída, mas se mais de um thread gravar em um fluxo por vez, a mistura arbitrária de dados dos diferentes threads normalmente tornará a saída bastante inútil.

Se você desativar a sincronização, a sincronização do acesso de vários encadeamentos também se tornará inteiramente de sua responsabilidade. Gravações simultâneas de vários threads podem / levarão a uma corrida de dados, o que significa que o código tem um comportamento indefinido.

Sumário

O C ++ assume como padrão uma tentativa de equilibrar velocidade com segurança. O resultado é bem-sucedido no código de thread único, mas menos no código de multi-thread. O código multithreaded normalmente precisa garantir que apenas um thread grave em um fluxo por vez para produzir uma saída útil.


1. É possível desativar o buffer para um fluxo, mas na verdade isso é bastante incomum, e quando / se alguém o faz, é provavelmente por um motivo bastante específico, como garantir que toda a saída seja capturada imediatamente, apesar do efeito no desempenho . De qualquer forma, isso só acontece se o código o fizer explicitamente.

Jerry Coffin
fonte
13
" Em linguagens de nível superior, como C e C ++, isso é menos problemático do que em Java. " - o que? C e C ++ são linguagens de nível inferior ao Java. Além disso, você esqueceu seus terminadores de linha.
User2357112 suporta Monica
1
Ao longo, aponto para a base objetiva do Java ser a linguagem de nível inferior. Não tenho certeza de que terminadores de linha você está falando.
Jerry Coffin
2
Java também faz concatenação em tempo de compilação. Por exemplo, "2^31 - 1 = " + Integer.MAX_VALUEé armazenado como uma única cadeia interna (JLS Sec 3.10.5 e 15.28 ).
200_success
2
@ 200_success: Java executando a concatenação de strings no tempo de compilação parece descer para §15.18.1: "O objeto String foi criado recentemente (§12.5), a menos que a expressão seja uma expressão constante em tempo de compilação (§15.28)." Isso parece permitir, mas não exige, que a concatenação seja feita em tempo de compilação. Ou seja, o resultado deve ser criado novamente se as entradas não forem constantes em tempo de compilação, mas nenhum requisito será feito em qualquer direção se forem constantes em tempo de compilação. Para exigir concatenação em tempo de compilação, você teria que ler seu (implícito) "se" como realmente significa "se e somente se".
Jerry Coffin
2
@Phoshi: Tente com recursos não é nem um pouco parecido com o RAII. RAII permite que a classe gerencie os recursos, mas tentar com recursos requer o código do cliente para gerenciar os recursos. Os recursos (abstrações, mais precisamente) que um possui e o outro não são totalmente relevantes - na verdade, são exatamente o que torna um idioma mais alto que o outro.
Jerry Coffin
1

Embora o desempenho não seja realmente um problema aqui, a má legibilidade de várias printlninstruções aponta para um aspecto de design ausente.

Por que escrevemos uma sequência de muitas printlndeclarações? Se fosse apenas um bloco de texto fixo, como um --helptexto em um comando do console, seria muito melhor tê-lo como um recurso separado, lê-lo e gravá-lo na tela, mediante solicitação.

Mas geralmente é uma mistura de partes dinâmicas e estáticas. Digamos que temos alguns dados de pedidos simples, por um lado, e algumas partes fixas de texto estático, por outro lado, e essas coisas precisam ser combinadas para formar uma folha de confirmação de pedido. Novamente, também neste caso, é melhor ter um arquivo de texto de recursos diferentes: O recurso seria um modelo, contendo algum tipo de símbolos (espaços reservados), que são substituídos no tempo de execução pelos dados reais do pedido.

A separação da linguagem de programação da linguagem natural tem muitas vantagens - entre elas a internacionalização: você pode precisar traduzir o texto se quiser se tornar multilíngue com o seu software. Além disso, por que uma etapa de compilação é necessária se você deseja apenas uma correção de texto, diga corrigir algum erro de ortografia.

rplantiko
fonte