É melhor reutilizar um StringBuilder em um loop?

101

Tenho uma pergunta relacionada ao desempenho em relação ao uso do StringBuilder. Em um loop muito longo, estou manipulando um StringBuildere passando-o para outro método como este:

for (loop condition) {
    StringBuilder sb = new StringBuilder();
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}

A instanciação StringBuilderem cada ciclo de loop é uma boa solução? E chamar um delete é melhor, como o seguinte?

StringBuilder sb = new StringBuilder();
for (loop condition) {
    sb.delete(0, sb.length);
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}
Pier Luigi
fonte

Respostas:

69

O segundo é cerca de 25% mais rápido no meu mini-benchmark.

public class ScratchPad {

    static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        for( int i = 0; i < 10000000; i++ ) {
            StringBuilder sb = new StringBuilder();
            sb.append( "someString" );
            sb.append( "someString2"+i );
            sb.append( "someStrin4g"+i );
            sb.append( "someStr5ing"+i );
            sb.append( "someSt7ring"+i );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for( int i = 0; i < 10000000; i++ ) {
            sb.delete( 0, sb.length() );
            sb.append( "someString" );
            sb.append( "someString2"+i );
            sb.append( "someStrin4g"+i );
            sb.append( "someStr5ing"+i );
            sb.append( "someSt7ring"+i );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
    }
}

Resultados:

25265
17969

Observe que isso é com JRE 1.6.0_07.


Com base nas idéias de Jon Skeet na edição, aqui está a versão 2. Mesmo assim.

public class ScratchPad {

    static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for( int i = 0; i < 10000000; i++ ) {
            sb.delete( 0, sb.length() );
            sb.append( "someString" );
            sb.append( "someString2" );
            sb.append( "someStrin4g" );
            sb.append( "someStr5ing" );
            sb.append( "someSt7ring" );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        for( int i = 0; i < 10000000; i++ ) {
            StringBuilder sb2 = new StringBuilder();
            sb2.append( "someString" );
            sb2.append( "someString2" );
            sb2.append( "someStrin4g" );
            sb2.append( "someStr5ing" );
            sb2.append( "someSt7ring" );
            a = sb2.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
    }
}

Resultados:

5016
7516
Epaga
fonte
4
Eu adicionei uma edição em minha resposta para explicar por que isso pode estar acontecendo. Vou olhar com mais atenção daqui a pouco (45 minutos). Observe que fazer concatenação nas chamadas de acréscimo reduz um pouco o ponto de usar StringBuilder em primeiro lugar :)
Jon Skeet
3
Também seria interessante ver o que acontece se você inverter os dois blocos - o JIT ainda está "aquecendo" o StringBuilder durante o primeiro teste. Pode ser irrelevante, mas é interessante tentar.
Jon Skeet,
1
Eu ainda iria com a primeira versão porque é mais limpa . Mas é bom que você realmente tenha feito o benchmark :) Próxima mudança sugerida: tente # 1 com uma capacidade apropriada passada para o construtor.
Jon Skeet,
25
Use sb.setLength (0); em vez disso, é a maneira mais rápida de esvaziar o conteúdo de StringBuilder contra a recriação do objeto ou usando .delete (). Observe que isso não se aplica a StringBuffer, suas verificações de simultaneidade anulam a vantagem de velocidade.
P Arrayah
1
Resposta ineficiente. P Arrayah e Dave Jarvis estão corretos. setLength (0) é de longe a resposta mais eficiente. StringBuilder é apoiado por uma matriz char e é mutável. No ponto em que .toString () é chamado, o array char é copiado e usado para apoiar uma string imutável. Neste ponto, o buffer mutável de StringBuilder pode ser reutilizado, simplesmente movendo o ponteiro de inserção de volta para zero (via .setLength (0)). sb.toString cria ainda outra cópia (o array imutável char), de modo que cada iteração requer dois buffers, em oposição ao método .setLength (0) que requer apenas um novo buffer por loop.
Chris
25

Na filosofia de escrever código sólido, é sempre melhor colocar o StringBuilder dentro do seu loop. Desta forma, não sai do código a que se destina.

Em segundo lugar, a maior melhoria no StringBuilder vem de dar a ele um tamanho inicial para evitar que cresça enquanto o loop é executado

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}
Peter
fonte
1
Você sempre pode definir o escopo da coisa toda com colchetes, dessa forma você não tem o Stringbuilder fora.
Epaga
@Epaga: Ainda está fora do próprio loop. Sim, isso não polui o escopo externo, mas é uma maneira não natural de escrever o código para uma melhoria de desempenho que não foi verificada no contexto .
Jon Skeet,
Ou melhor ainda, coloque tudo em seu próprio método. ;-) Mas eu ouvi dizer: contexto.
Epaga
Melhor ainda inicializar com o tamanho esperado em vez da soma do número arbitrário (4096) Seu código pode retornar uma String que faz referência a um char [] de tamanho 4096 (depende do JDK; pelo que me lembro, esse era o caso de 1.4)
kohlerm
24

Mais rápido, ainda:

public class ScratchPad {

    private static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder( 128 );

        for( int i = 0; i < 10000000; i++ ) {
            // Resetting the string is faster than creating a new object.
            // Since this is a critical loop, every instruction counts.
            //
            sb.setLength( 0 );
            sb.append( "someString" );
            sb.append( "someString2" );
            sb.append( "someStrin4g" );
            sb.append( "someStr5ing" );
            sb.append( "someSt7ring" );
            setA( sb.toString() );
        }

        System.out.println( System.currentTimeMillis()-time );
    }

    private static void setA( String aString ) {
        a = aString;
    }
}

Na filosofia de escrever código sólido, o funcionamento interno do método deve ser escondido dos objetos que usam o método. Portanto, não faz diferença da perspectiva do sistema se você redeclarar o StringBuilder dentro do loop ou fora do loop. Uma vez que declará-lo fora do loop é mais rápido e não torna o código mais complicado de ler, reutilize o objeto em vez de restabelecê-lo.

Mesmo se o código fosse mais complicado e você soubesse com certeza que a instanciação do objeto era o gargalo, comente-o.

Três corridas com esta resposta:

$ java ScratchPad
1567
$ java ScratchPad
1569
$ java ScratchPad
1570

Três corridas com a outra resposta:

$ java ScratchPad2
1663
2231
$ java ScratchPad2
1656
2233
$ java ScratchPad2
1658
2242

Embora não seja significativo, definir o StringBuildertamanho do buffer inicial proporcionará um pequeno ganho.

Dave Jarvis
fonte
3
Esta é de longe a melhor resposta. StringBuilder é apoiado por uma matriz char e é mutável. No ponto em que .toString () é chamado, o array char é copiado e usado para apoiar uma string imutável. Neste ponto, o buffer mutável de StringBuilder pode ser reutilizado, simplesmente movendo o ponteiro de inserção de volta para zero (via .setLength (0)). Essas respostas sugerindo alocar um novo StringBuilder por loop não parecem perceber que .toString cria ainda outra cópia, então cada iteração requer dois buffers em oposição ao método .setLength (0) que requer apenas um novo buffer por loop.
Chris
12

Ok, agora eu entendo o que está acontecendo e faz sentido.

Fiquei com a impressão de que toStringacabei de passar o subjacente char[]para um construtor String que não tirou uma cópia. Uma cópia seria feita na próxima operação de "gravação" (por exemplo delete). Acredito que foi esse o caso com StringBufferalguma versão anterior. (Não é agora.) Mas não - toStringapenas passa a matriz (e índice e comprimento) para o Stringconstrutor público que faz uma cópia.

Portanto, no caso de "reutilizar StringBuilder", criamos genuinamente uma cópia dos dados por string, usando o mesmo array char no buffer o tempo todo. Obviamente, criar um novo a StringBuildercada vez cria um novo buffer subjacente - e então esse buffer é copiado (um tanto sem sentido, em nosso caso particular, mas feito por razões de segurança) ao criar uma nova string.

Tudo isso faz com que a segunda versão seja definitivamente mais eficiente - mas ao mesmo tempo, ainda diria que é um código mais feio.

Jon Skeet
fonte
Apenas algumas informações engraçadas sobre o .NET, a situação é diferente. O .NET StringBuilder modifica internamente o objeto "string" regular e o método toString simplesmente o retorna (marcando-o como não modificável, portanto, as manipulações subsequentes do StringBuilder irão recriá-lo). Portanto, a sequência típica "novo StringBuilder-> modificá-lo-> para String" não fará nenhuma cópia extra (apenas para expandir o armazenamento ou diminuí-lo, se o comprimento da string resultante for muito menor do que sua capacidade). Em Java, este ciclo sempre faz pelo menos uma cópia (em StringBuilder.toString ()).
Ivan Dubrov
O Sun JDK pré-1.5 tinha a otimização que você estava assumindo: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959
Dan Berindei
9

Como acho que ainda não foi apontado, por causa das otimizações integradas ao compilador Sun Java, que cria automaticamente StringBuilders (StringBuffers pré-J2SE 5.0) quando vê concatenações de String, o primeiro exemplo na pergunta é equivalente a:

for (loop condition) {
  String s = "some string";
  . . .
  s += anotherString;
  . . .
  passToMethod(s);
}

O que é mais legível, IMO, a melhor abordagem. Suas tentativas de otimizar podem resultar em ganhos em algumas plataformas, mas potencialmente em perdas em outras.

Mas se você realmente está enfrentando problemas de desempenho, então, com certeza, otimize. Eu começaria especificando explicitamente o tamanho do buffer do StringBuilder, de acordo com Jon Skeet.

Jack Leow
fonte
4

A JVM moderna é muito inteligente sobre coisas como essa. Eu não iria duvidar e fazer algo hacky que é menos fácil de manter / legível ... a menos que você faça benchmarks adequados com dados de produção que validem uma melhoria de desempenho não trivial (e a documente;)

Stu Thompson
fonte
Onde "não trivial" é a chave - os benchmarks podem mostrar uma forma sendo proporcionalmente mais rápida, mas sem nenhuma dica de quanto tempo isso está levando no aplicativo real :)
Jon Skeet
Veja o benchmark em minha resposta abaixo. A segunda maneira é mais rápida.
Epaga
1
@Epaga: Seu benchmark diz pouco sobre a melhoria de desempenho no aplicativo real, onde o tempo gasto para fazer a alocação do StringBuilder pode ser trivial em comparação com o resto do loop. É por isso que o contexto é importante no benchmarking.
Jon Skeet,
1
@Epaga: Até que ele tenha medido com seu código real, não teremos ideia de como ele realmente é significativo. Se houver muito código para cada iteração do loop, suspeito fortemente que ainda será irrelevante. Não sabemos o que há no "..."
Jon Skeet
1
(Não me interpretem mal, aliás - seus resultados de benchmark ainda são muito interessantes por si só. Sou fascinado por microbenchmarks. Só não gosto de dobrar meu código fora de forma antes de realizar testes da vida real também.)
Jon Skeet,
4

Com base na minha experiência com o desenvolvimento de software no Windows, eu diria que limpar o StringBuilder durante o loop tem melhor desempenho do que instanciar um StringBuilder com cada iteração. Limpá-lo libera a memória para ser sobrescrita imediatamente, sem a necessidade de alocação adicional. Não estou familiarizado o suficiente com o coletor de lixo Java, mas acho que a liberação e nenhuma realocação (a menos que sua próxima string aumente o StringBuilder) é mais benéfico do que a instanciação.

(Minha opinião é contrária ao que todo mundo está sugerindo. Hmm. É hora de compará-la.)

Cfeduke
fonte
O fato é que mais memória deve ser realocada de qualquer maneira, pois os dados existentes estão sendo usados ​​pela String recém-criada no final da iteração do loop anterior.
Jon Skeet,
Oh, isso faz sentido, pensei que toString estava alocando e retornando uma nova instância de string e o buffer de bytes para o construtor estava limpando em vez de realocar.
cfeduke
O benchmark da Epaga mostra que limpar e reutilizar é um ganho sobre a instanciação em cada passagem.
cfeduke
1

A razão pela qual fazer um 'setLength' ou 'deletar' melhora o desempenho é principalmente o código 'aprendendo' o tamanho certo do buffer, e menos para fazer a alocação de memória. Geralmente, eu recomendo deixar o compilador fazer as otimizações de string . No entanto, se o desempenho for crítico, geralmente pré-calcularei o tamanho esperado do buffer. O tamanho padrão do StringBuilder é de 16 caracteres. Se você crescer além disso, será necessário redimensionar. O redimensionamento é onde o desempenho está sendo perdido. Aqui está outro mini-benchmark que ilustra isso:

private void clear() throws Exception {
    long time = System.currentTimeMillis();
    int maxLength = 0;
    StringBuilder sb = new StringBuilder();

    for( int i = 0; i < 10000000; i++ ) {
        // Resetting the string is faster than creating a new object.
        // Since this is a critical loop, every instruction counts.
        //
        sb.setLength( 0 );
        sb.append( "someString" );
        sb.append( "someString2" ).append( i );
        sb.append( "someStrin4g" ).append( i );
        sb.append( "someStr5ing" ).append( i );
        sb.append( "someSt7ring" ).append( i );
        maxLength = Math.max(maxLength, sb.toString().length());
    }

    System.out.println(maxLength);
    System.out.println("Clear buffer: " + (System.currentTimeMillis()-time) );
}

private void preAllocate() throws Exception {
    long time = System.currentTimeMillis();
    int maxLength = 0;

    for( int i = 0; i < 10000000; i++ ) {
        StringBuilder sb = new StringBuilder(82);
        sb.append( "someString" );
        sb.append( "someString2" ).append( i );
        sb.append( "someStrin4g" ).append( i );
        sb.append( "someStr5ing" ).append( i );
        sb.append( "someSt7ring" ).append( i );
        maxLength = Math.max(maxLength, sb.toString().length());
    }

    System.out.println(maxLength);
    System.out.println("Pre allocate: " + (System.currentTimeMillis()-time) );
}

public void testBoth() throws Exception {
    for(int i = 0; i < 5; i++) {
        clear();
        preAllocate();
    }
}

Os resultados mostram que reutilizar o objeto é cerca de 10% mais rápido do que criar um buffer do tamanho esperado.

brianegge
fonte
1

LOL, a primeira vez que vi pessoas comparando o desempenho combinando string no StringBuilder. Para isso, se você usar "+", pode ser ainda mais rápido; D. O objetivo de usar StringBuilder para acelerar a recuperação de toda a string como o conceito de "localidade".

No cenário em que você recupera um valor String com frequência que não precisa de alteração frequente, o Stringbuilder permite um desempenho superior de recuperação de string. E esse é o propósito de usar o Stringbuilder .. por favor, não teste MIS o propósito principal disso ..

Algumas pessoas disseram: O avião voa mais rápido. Portanto, testei com minha bicicleta e descobri que o avião se move mais devagar. Você sabe como eu defino as configurações do experimento; D

Ting Choo Chiaw
fonte
1

Não significativamente mais rápido, mas em meus testes ele mostra ser, em média, alguns milissegundos mais rápido usando 1.6.0_45 64 bits: use StringBuilder.setLength (0) em vez de StringBuilder.delete ():

time = System.currentTimeMillis();
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < 10000000; i++) {
    sb2.append( "someString" );
    sb2.append( "someString2"+i );
    sb2.append( "someStrin4g"+i );
    sb2.append( "someStr5ing"+i );
    sb2.append( "someSt7ring"+i );
    a = sb2.toString();
    sb2.setLength(0);
}
System.out.println( System.currentTimeMillis()-time );
johnmartel
fonte
1

A maneira mais rápida é usar "setLength". Não envolverá a operação de cópia. A maneira de criar um novo StringBuilder deve estar completamente fora . A lentidão para StringBuilder.delete (int start, int end) é porque ele copiará a matriz novamente para a parte de redimensionamento.

 System.arraycopy(value, start+len, value, start, count-end);

Depois disso, o StringBuilder.delete () atualizará o StringBuilder.count para o novo tamanho. Enquanto StringBuilder.setLength () simplifica a atualização de StringBuilder.count para o novo tamanho.

Shen liang
fonte
0

O primeiro é melhor para os humanos. Se o segundo for um pouco mais rápido em algumas versões de algumas JVMs, e daí?

Se o desempenho for tão crítico, ignore o StringBuilder e escreva o seu próprio. Se você é um bom programador e leva em consideração como seu aplicativo está usando esta função, você deve ser capaz de torná-la ainda mais rápida. Que vale a pena? Provavelmente não.

Por que essa pergunta foi marcada como "pergunta favorita"? Porque a otimização de desempenho é muito divertida, não importa se é prática ou não.

dongilmore
fonte
Não é apenas uma questão acadêmica. Embora na maioria das vezes (leia 95%) eu prefira a legibilidade e a manutenção, existem realmente casos em que pequenas melhorias fazem grandes diferenças ...
Pier Luigi
OK, vou mudar minha resposta. Se um objeto fornece um método que permite que ele seja limpo e reutilizado, faça isso. Examine o código primeiro se quiser ter certeza de que a limpeza é eficiente; talvez ele libere um array privado! Se eficiente, aloque o objeto fora do loop e reutilize-o dentro.
dongilmore
0

Não acho que faça sentido tentar otimizar o desempenho dessa forma. Hoje (2019) os dois estados estão rodando cerca de 11 segundos para 100.000.000 de loops no meu laptop I5:

    String a;
    StringBuilder sb = new StringBuilder();
    long time = 0;

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
        sb3.append("someString2");
        sb3.append("someStrin4g");
        sb3.append("someStr5ing");
        sb3.append("someSt7ring");
        a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        sb.append("someString2");
        sb.append("someStrin4g");
        sb.append("someStr5ing");
        sb.append("someSt7ring");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 11000 mseg (declaração dentro do loop) e 8236 mseg (declaração fora do loop)

Mesmo que eu esteja executando programas para dedublicação de endereços com alguns bilhões de loops, uma diferença de 2 segundos. para 100 milhões de loops não faz nenhuma diferença porque os programas ficam em execução por horas. Também esteja ciente de que as coisas são diferentes se você tiver apenas uma instrução append:

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
            a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 3416 mseg (loop interno), 3555 mseg (loop externo) A primeira instrução que está criando o StringBuilder dentro do loop é mais rápida nesse caso. E, se você alterar a ordem de execução, é muito mais rápido:

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
            a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 3638 mseg (loop externo), 2.908 mseg (loop interno)

Atenciosamente, Ulrich

Ulrich K.
fonte
-2

Declare uma vez e atribua a cada vez. É um conceito mais pragmático e reutilizável do que uma otimização.

Peter Mortensen
fonte