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ção+ tem 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.
**