Dicas de desempenho de strings

Dicas de desempenho de strings

**

1. Introdução

Neste tutorial,we’re going to focus on the performance aspect of the Java String API.

Investigaremos as operações de criação, conversão e modificação deString para analisar as opções disponíveis e comparar sua eficiência.

As sugestões que faremos não serão necessariamente as adequadas para cada aplicação. Mas certamente, vamos mostrar como ganhar no desempenho quando o tempo de execução do aplicativo é crítico.

2. Construindo uma nova corda

Como você sabe, em Java, Strings são imutáveis. Portanto, toda vez que construímos ou concatenamos um objetoString, Java cria um novoString –, isso pode ser especialmente caro se for feito em um loop.

2.1. Usando Construtor

Na maioria dos casos,we should avoid creating Strings using the constructor unless we know what are we doing.

Vamos criar umnewString objeto dentro do loop primeiro, usando o construtornew String() e, em seguida, o operador=.

Para escrever nosso benchmark, usaremos a ferramentaJMH (Java Microbenchmark Harness).

Nossa configuração:

@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
public class StringPerformance {
}

Aqui, estamos usando o modoSingeShotTime, que executa o método apenas uma vez. Como queremos medir o desempenho das operaçõesString dentro do loop, há uma anotação@Measurement disponível para isso.

É importante saber quebenchmarking loops directly in our tests may skew the results because of various optimizations applied by JVM.

Portanto, calculamos apenas a operação única e deixamos a JMH cuidar do loop. Resumidamente, o JMH executa as iterações usando o parâmetrobatchSize.

Agora, vamos adicionar o primeiro micro-benchmark:

@Benchmark
public String benchmarkStringConstructor() {
    return new String("example");
}

@Benchmark
public String benchmarkStringLiteral() {
    return "example";
}

No primeiro teste, um novo objeto é criado em cada iteração. No segundo teste, o objeto é criado apenas uma vez. Para as iterações restantes, o mesmo objeto é retornado do pool de constantesString’s.

Vamos executar os testes com a contagem de iterações de loop= 1,000,000e ver os resultados:

Benchmark                   Mode  Cnt  Score    Error     Units
benchmarkStringConstructor  ss     10  16.089 ± 3.355     ms/op
benchmarkStringLiteral      ss     10  9.523  ± 3.331     ms/op

A partir dos valores deScore, podemos ver claramente que a diferença é significativa.

2.2. + Operador

Vamos dar uma olhada no exemplo de concatenação dinâmicaString:

@State(Scope.Thread)
public static class StringPerformanceHints {
    String result = "";
    String example = "example";
}

@Benchmark
public String benchmarkStringDynamicConcat() {
    return result + example;
}

Em nossos resultados, queremos ver o tempo médio de execução. O formato do número de saída está definido para milissegundos:

Benchmark                       1000     10,000
benchmarkStringDynamicConcat    47.331   4370.411

Agora, vamos analisar os resultados. Como vemos, adicionar1000 itens astate.result leva47.331 milissegundos. Consequentemente, aumentando o número de iterações em 10 vezes, o tempo de execução sobe para4370.441 milissegundos.

Em resumo, o tempo de execução cresce quadraticamente. Portanto, a complexidade da concatenação dinâmica em um loop de n iterações éO(n^2).

2.3. String.concat()

Outra maneira de concatenarStrings é usando o métodoconcat():

@Benchmark
public String benchmarkStringConcat() {
    return result.concat(example);
}

A unidade de tempo de saída é um milissegundo, a contagem de iterações é 100.000. A tabela de resultados se parece com:

Benchmark              Mode  Cnt  Score     Error     Units
benchmarkStringConcat    ss   10  3403.146 ± 852.520  ms/op

2.4. String.format()

Outra maneira de criar strings é usando o métodoString.format(). Under the hood, it uses regular expressions to parse the input.

Vamos escrever o caso de teste JMH:

String formatString = "hello %s, nice to meet you";

@Benchmark
public String benchmarkStringFormat_s() {
    return String.format(formatString, example);
}

Depois, executamos e vemos os resultados:

Number of Iterations      10,000   100,000   1,000,000
benchmarkStringFormat_s   17.181   140.456   1636.279    ms/op

Embora o código comString.format() pareça mais limpo e legível, não ganhamos aqui em termos de desempenho.

2.5. StringBuilder eStringBuffer

Já temos umwrite-up explicandoStringBuffereStringBuilder. Então, aqui, vamos mostrar apenas informações extras sobre seu desempenho. StringBuilder uses uma matriz redimensionável e um índice que indica a posição da última célula usada na matriz. Quando a matriz está cheia, ela expande o dobro do tamanho e copia todos os caracteres para a nova matriz.

Levando em consideração que o redimensionamento não ocorre com muita frequência,we can consider each append() operation as O(1) constant time. Levando isso em consideração, todo o processo apresentaO(n) de complexidade.

Depois de modificar e executar o teste de concatenação dinâmica paraStringBuffereStringBuilder, we get:

Benchmark               Mode  Cnt  Score   Error  Units
benchmarkStringBuffer   ss    10  1.409  ± 1.665  ms/op
benchmarkStringBuilder  ss    10  1.200  ± 0.648  ms/op

Embora a diferença de pontuação não seja muito grande, podemos notarthat StringBuilder works faster.

Felizmente, em casos simples, não precisamos deStringBuilder para colocar umString com outro. Às vezes,static concatenation with + can actually replace StringBuilder. Under the hood, the latest Java compilers will call the StringBuilder.append() to concatenate strings.

Isso significa ganhar significativamente no desempenho.

3. Operações de Utilidades

3.1. StringUtils.replace() vsString.replace()

É interessante saber queApache Commons version for replacing the String does way better than the String’s own replace() method. A resposta para essa diferença está na sua implementação. String.replace() usa um padrão regex para corresponder aoString.

Em contraste,StringUtils.replace() usa amplamenteindexOf(), que é mais rápido.

Agora, é hora dos testes de benchmark:

@Benchmark
public String benchmarkStringReplace() {
    return longString.replace("average", " average !!!");
}

@Benchmark
public String benchmarkStringUtilsReplace() {
    return StringUtils.replace(longString, "average", " average !!!");
}

DefinindobatchSize para 100.000, apresentamos os resultados:

Benchmark                     Mode  Cnt  Score   Error   Units
benchmarkStringReplace         ss   10   6.233  ± 2.922  ms/op
benchmarkStringUtilsReplace    ss   10   5.355  ± 2.497  ms/op

Embora a diferença entre os números não seja muito grande, oStringUtils.replace() tem uma pontuação melhor. Obviamente, os números e o espaço entre eles podem variar dependendo de parâmetros como contagem de iterações, comprimento da string e até a versão do JDK.

Com as versões mais recentes do JDK 9+ (nossos testes estão sendo executados no JDK 10), ambas as implementações têm resultados bastante iguais. Agora, vamos fazer o downgrade da versão JDK para 8 e os testes novamente:

Benchmark                     Mode  Cnt   Score    Error     Units
benchmarkStringReplace         ss   10    48.061   ± 17.157  ms/op
benchmarkStringUtilsReplace    ss   10    14.478   ±  5.752  ms/op

A diferença de desempenho é enorme agora e confirma a teoria que discutimos no começo.

3.2. split()

Antes de começar, será útil verificar a stringsplitting methods disponível em Java.

Quando há necessidade de dividir uma string com o delimitador, a primeira função que vem à nossa mente geralmente éString.split(regex). No entanto, traz alguns problemas sérios de desempenho, pois aceita um argumento regex. Como alternativa, podemos usar a classeStringTokenizer para quebrar a string em tokens.

Outra opção é a APISplitter do Guava. Finalmente, o bom e velhoindexOf() também está disponível para impulsionar o desempenho de nosso aplicativo se não precisarmos da funcionalidade de expressões regulares.

Agora, é hora de escrever os testes de benchmark para a opçãoString.split():

String emptyString = " ";

@Benchmark
public String [] benchmarkStringSplit() {
    return longString.split(emptyString);
}

Pattern.split():

@Benchmark
public String [] benchmarkStringSplitPattern() {
    return spacePattern.split(longString, 0);
}

StringTokenizer:

List stringTokenizer = new ArrayList<>();

@Benchmark
public List benchmarkStringTokenizer() {
    StringTokenizer st = new StringTokenizer(longString);
    while (st.hasMoreTokens()) {
        stringTokenizer.add(st.nextToken());
    }
    return stringTokenizer;
}

String.indexOf():

List stringSplit = new ArrayList<>();

@Benchmark
public List benchmarkStringIndexOf() {
    int pos = 0, end;
    while ((end = longString.indexOf(' ', pos)) >= 0) {
        stringSplit.add(longString.substring(pos, end));
        pos = end + 1;
    }
    return stringSplit;
}

Splitter da goiaba:

@Benchmark
public List benchmarkGuavaSplitter() {
    return Splitter.on(" ").trimResults()
      .omitEmptyStrings()
      .splitToList(longString);
}

Finalmente, executamos e comparamos os resultados parabatchSize = 100,000:

Benchmark                     Mode  Cnt    Score    Error    Units
benchmarkGuavaSplitter         ss   10    4.008  ± 1.836     ms/op
benchmarkStringIndexOf         ss   10    1.144  ± 0.322     ms/op
benchmarkStringSplit           ss   10    1.983  ± 1.075     ms/op
benchmarkStringSplitPattern    ss   10    14.891  ± 5.678    ms/op
benchmarkStringTokenizer       ss   10    2.277  ± 0.448     ms/op

Como podemos ver, o pior desempenho tem o métodobenchmarkStringSplitPattern, onde usamos a classePattern. Como resultado, podemos aprender que usar uma classe regex com o métodosplit() pode causar perda de desempenho várias vezes.

Da mesma forma,we notice that the fastest results are providing examples with the use of indexOf() and split().

3.3. Convertendo paraString

Nesta seção, vamos medir as pontuações de tempo de execução da conversão de string. Para ser mais específico, examinaremos o método de concatenaçãoInteger.toString():

int sampleNumber = 100;

@Benchmark
public String benchmarkIntegerToString() {
    return Integer.toString(sampleNumber);
}

String.valueOf():

@Benchmark
public String benchmarkStringValueOf() {
    return String.valueOf(sampleNumber);
}

[some integer value] + “”:

@Benchmark
public String benchmarkStringConvertPlus() {
    return sampleNumber + "";
}

String.format():

String formatDigit = "%d";

@Benchmark
public String benchmarkStringFormat_d() {
    return String.format(formatDigit, sampleNumber);
}

Depois de executar os testes, veremos a saída parabatchSize = 10,000:

Benchmark                     Mode  Cnt   Score    Error  Units
benchmarkIntegerToString      ss   10   0.953 ±  0.707  ms/op
benchmarkStringConvertPlus    ss   10   1.464 ±  1.670  ms/op
benchmarkStringFormat_d       ss   10  15.656 ±  8.896  ms/op
benchmarkStringValueOf        ss   10   2.847 ± 11.153  ms/op

Depois de analisar os resultados, vemos quethe test for Integer.toString() has the best score of 0.953 milliseconds. Em contraste, uma conversão que envolveString.format(“%d”) tem o pior desempenho.

Isso é lógico porque analisar o formatoString é uma operação cara.

3.4. Comparando strings

Vamos avaliar diferentes maneiras de compararStrings. A contagem de iterações é100,000.

Aqui estão nossos testes de benchmark para a operaçãoString.equals():

@Benchmark
public boolean benchmarkStringEquals() {
    return longString.equals(example);
}

String.equalsIgnoreCase():

@Benchmark
public boolean benchmarkStringEqualsIgnoreCase() {
    return longString.equalsIgnoreCase(example);
}

String.matches():

@Benchmark
public boolean benchmarkStringMatches() {
    return longString.matches(example);
}

String.compareTo():

@Benchmark
public int benchmarkStringCompareTo() {
    return longString.compareTo(example);
}

Depois, executamos os testes e exibimos os resultados:

Benchmark                         Mode  Cnt    Score    Error  Units
benchmarkStringCompareTo           ss   10    2.561 ±  0.899   ms/op
benchmarkStringEquals              ss   10    1.712 ±  0.839   ms/op
benchmarkStringEqualsIgnoreCase    ss   10    2.081 ±  1.221   ms/op
benchmarkStringMatches             ss   10    118.364 ± 43.203 ms/op

Como sempre, os números falam por si. Omatches() é o mais demorado, pois usa a regex para comparar a igualdade.

Em contraste,the equals() and equalsIgnoreCase() are the best choices.

3.5. String.matches() vsPrecompiled Pattern

Agora, vamos dar uma olhada separada emString.matches()eMatcher.matches() . O primeiro pega um regexp como argumento e o compila antes de executar.

Então, toda vez que chamamosString.matches(), ele compila oPattern:

@Benchmark
public boolean benchmarkStringMatches() {
    return longString.matches(example);
}

O segundo método reutiliza o objetoPattern:

Pattern longPattern = Pattern.compile(longString);

@Benchmark
public boolean benchmarkPrecompiledMatches() {
    return longPattern.matcher(example).matches();
}

E agora os resultados:

Benchmark                      Mode  Cnt    Score    Error   Units
benchmarkPrecompiledMatches    ss   10    29.594  ± 12.784   ms/op
benchmarkStringMatches         ss   10    106.821 ± 46.963   ms/op

Como vemos, a correspondência com o regexp pré-compilado funciona três vezes mais rápido.

3.6. Verificando o comprimento

Finalmente, vamos comparar o métodoString.isEmpty():

@Benchmark
public boolean benchmarkStringIsEmpty() {
    return longString.isEmpty();
}

e o métodoString.length():

@Benchmark
public boolean benchmarkStringLengthZero() {
    return emptyString.length() == 0;
}

Primeiro, nós os chamamos delongString = “Hello example, I am a bit longer than other Strings in average” String. ObatchSize é10,000:

Benchmark                  Mode  Cnt  Score   Error  Units
benchmarkStringIsEmpty       ss   10  0.295 ± 0.277  ms/op
benchmarkStringLengthZero    ss   10  0.472 ± 0.840  ms/op

Depois, vamos definir a string vazialongString = “” e executar os testes novamente:

Benchmark                  Mode  Cnt  Score   Error  Units
benchmarkStringIsEmpty       ss   10  0.245 ± 0.362  ms/op
benchmarkStringLengthZero    ss   10  0.351 ± 0.473  ms/op

Como notamos, os métodosbenchmarkStringLengthZero()ebenchmarkStringIsEmpty() em ambos os casos têm aproximadamente a mesma pontuação. No entanto, chamandoisEmpty() works faster than checking if the string’s length is zero.

4. Desduplicação de string

Desde o JDK 8, o recurso de deduplicação de sequência está disponível para eliminar o consumo de memória. Simplificando,this tool is looking for the strings with the same or duplicate contents to store one copy of each distinct string value into the String pool.

Atualmente, existem duas maneiras de lidar com duplicatas deString:

  • usando oString.intern() manualmente

  • ativando a desduplicação de string

Vamos dar uma olhada em cada opção.

4.1. String.intern()

Antes de avançar, será útil ler sobre o internamento manual em nossowrite-up. With String.intern() we can manually set the reference of the String object inside of the global String pool.

Em seguida, a JVM pode usar return a referência quando necessário. Do ponto de vista do desempenho, nosso aplicativo pode se beneficiar imensamente, reutilizando as referências de string do pool constante.

É importante saber queJVM String pool isn’t local for the thread. Each String that we add to the pool, is available to other threads as well.

No entanto, também existem sérias desvantagens:

  • para manter nosso aplicativo adequadamente, podemos precisar definir um parâmetro-XX:StringTableSize JVM para aumentar o tamanho do pool. A JVM precisa de uma reinicialização para expandir o tamanho do conjunto

  • calling String.intern() manually is time-consuming. Ele cresce em um algoritmo de tempo linear com complexidade deO(n)

  • adicionalmente,frequent calls on long String objects may cause memory problems

Para ter alguns números comprovados, vamos fazer um teste de benchmark:

@Benchmark
public String benchmarkStringIntern() {
    return example.intern();
}

Além disso, as pontuações de saída estão em milissegundos:

Benchmark               1000   10,000  100,000  1,000,000
benchmarkStringIntern   0.433  2.243   19.996   204.373

Os cabeçalhos das colunas aqui representam contagens deiterations diferentes de1000 a1,000,000. Para cada número de iteração, temos a pontuação de desempenho do teste. Como notamos, a pontuação aumenta dramaticamente além do número de iterações.

4.2. Habilitar desduplicação automaticamente

Em primeiro lugar,this option is a part of the G1 garbage collector. Por padrão, este recurso está desabilitado. Portanto, precisamos habilitá-lo com o seguinte comando:

 -XX:+UseG1GC -XX:+UseStringDeduplication

É importante observar queenabling this option doesn’t guarantee that String deduplication will happen. Além disso, ele não processaStrings. jovem para gerenciar a idade mínima de processamentoStrings, XX:StringDeduplicationAgeThreshold=3, a opção JVM está disponível. Aqui,3 é o parâmetro padrão.

5. Sumário

Neste tutorial, estamos tentando dar algumas dicas para usar strings de forma mais eficiente em nossa vida diária de codificação.

Como resultado,we can highlight some suggestions in order to boost our application performance:

  • when concatenating strings, the StringBuilder is the most convenient option que vem à mente. No entanto, com as strings pequenas, a operaçãotem quase o mesmo desempenho. Sob o capô, o compilador Java pode usar o sclassStringBuilder para reduzir o número de objetos de string

  • para converter o valor em string,[some type].toString() (Integer.toString() por exemplo) funciona mais rápido do queString.valueOf(). Como essa diferença não é significativa, podemos usar livrementeString.valueOf() para não ter uma dependência do tipo de valor de entrada

  • quando se trata de comparação de strings, nada supera oString.equals() até agora

  • A deduplicaçãoString melhora o desempenho em aplicativos grandes e multithread. Mas o uso excessivo deString.intern() pode causar sérios vazamentos de memória, tornando o aplicativo mais lento

  • for splitting the strings we should use indexOf() to win in performance. No entanto, em alguns casos não críticos, a funçãoString.split() pode ser um bom ajuste

  • UsandoPattern.match(), a string melhora o desempenho significativamente

  • String.isEmpty() é mais rápido que String.length() ==0

Além disso,keep in mind that the numbers we present here are just JMH benchmark results - portanto, você deve sempre testar no escopo de seu próprio sistema e tempo de execução para determinar o impacto desses tipos de otimizações.

Finalmente, como sempre, o código usado durante a discussão pode ser encontradoover on GitHub.

**