Cómo degradar el rendimiento al mejorarlo

Queríamos lo mejor, pero resultó como siempre.
Victor Chernomyrdin,
Estadista ruso


Hay momentos en la vida en los que pareces estar haciendo todo bien, pero algo sale mal.
Esta historia es sobre uno de esos casos.


Una vez que miré este código y pensé en acelerarlo:


public String appendBounds(Data data) { int beginIndex = data.beginIndex; int endIndex = data.endIndex; return new StringBuilder() .append('L') .append(data.str, beginIndex, endIndex) .append(';') .toString(); } 

Primero, quería calcular la longitud total de la cadena usando las variables beginIndex y endIndex (así como el hecho de que, además de la cadena truncada, se agregarán 2 caracteres más a StringBuilder ) y pasar este valor al constructor StringBuilder para seleccionar inmediatamente la matriz del tamaño requerido . Este pensamiento me pareció demasiado obvio, así que decidí probar otra cosa. El hecho de que este código no haya sido resaltado por la "Idea" me llevó a la idea correcta, aunque esta chica inteligente generalmente sugiere reemplazar la cadena corta de StringBuilder::append con la adición de cadenas, que es más corta y más fácil de leer.


Un obstáculo para esta simplificación es el uso del método StringBuilder.append(CharSequence, int, int) . Dado que el campo data.str es una cadena, con String.substring(beginIndex, endIndex) puede seleccionar una subcadena y pasarla a StringBuilder.append(String) .


Código después de la conversión:


 public String appendBounds(Data data) { int beginIndex = data.beginIndex; int endIndex = data.endIndex; String subString = data.str.substring(beginIndex, endIndex); return new StringBuilder() .append('L') .append(subString) .append(';') .toString(); } 

Y ahora la Idea ofrece una simplificación:


 public String appendBounds(Data data) { int beginIndex = data.beginIndex; int endIndex = data.endIndex; return 'L' + data.str.substring(beginIndex, endIndex) + ';'; } 

Sin embargo, nuestro objetivo en este caso no es tanto la legibilidad como la productividad. Compare ambos métodos:


 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"}) public class StringBuilderAppendBenchmark { @Benchmark public String appendSubString(Data data) { String latinStr = data.latinStr; String nonLatinStr = data.nonLatinStr; int beginIndex = data.beginIndex; int endIndex = data.endIndex; String substring = data.nonLatin ? nonLatinStr.substring(beginIndex, endIndex) : latinStr.substring(beginIndex, endIndex); return new StringBuilder() .append('L') .append(substring) .append(';') .toString(); } @Benchmark public String appendBounds(Data data) { String latinStr = data.latinStr; String nonLatinStr = data.nonLatinStr; int beginIndex = data.beginIndex; int endIndex = data.endIndex; String appended = data.nonLatin ? nonLatinStr : latinStr; return new StringBuilder() .append('L') .append(appended, beginIndex, endIndex) .append(';') .toString(); } @State(Scope.Thread) public static class Data { String latinStr; String nonLatinStr; @Param({"true", "false"}) boolean nonLatin; @Param({"5", "10", "50", "100", "500", "1000"}) private int length; private int beginIndex; private int endIndex; private ThreadLocalRandom random = ThreadLocalRandom.current(); @Setup public void setup() { latinStr = randomString("abcdefghijklmnopqrstuvwxyz"); nonLatinStr = randomString(""); beginIndex = 1; endIndex = length + 1; } private String randomString(String alphabet) { char[] chars = alphabet.toCharArray(); StringBuilder sb = new StringBuilder(length + 2); for (int i = 0; i < length + 2; i++) { char c = chars[random.nextInt(chars.length)]; sb.append(c); } return sb.toString(); } } } 

El punto de referencia es tan simple como dos centavos: se agrega una cadena aleatoria al StringBuilder , cuyo tamaño está determinado por el campo de length , y dado que el patio es 2019, debe verificarlo como una cadena que contiene solo los caracteres del alfabeto latino principal (la llamada línea comprimida, en la que cada carácter corresponde a 1 byte) y una cadena con caracteres no latinos (cada carácter está representado por 2 bytes).


En un examen superficial, el método appendSubString nos appendSubString más lento, porque la cantidad de datos a pegar coincide con la del método appendBounds , sin embargo, en el método appendSubString hay una creación explícita de una subcadena, es decir, asignar memoria para un nuevo objeto y copiar el contenido de data.latinStr en él / data.nonLatinStr .


Aún más sorprendente (pero solo a primera vista) los resultados de la medición realizada por mí usando JDK11 en una máquina doméstica (Intel Core i5-4690, 3.50 GHz) parecen ser:


 Benchmark nonLatin length Score Error Units appendBounds true 5 44,6 ± 0,4 ns/op appendBounds true 10 45,7 ± 0,7 ns/op appendBounds true 50 129,0 ± 0,5 ns/op appendBounds true 100 218,7 ± 0,8 ns/op appendBounds true 500 907,1 ± 5,5 ns/op appendBounds true 1000 1626,4 ± 13,0 ns/op appendSubString true 5 28,6 ± 0,2 ns/op appendSubString true 10 30,8 ± 0,2 ns/op appendSubString true 50 65,6 ± 0,4 ns/op appendSubString true 100 106,6 ± 0,6 ns/op appendSubString true 500 430,1 ± 2,4 ns/op appendSubString true 1000 839,1 ± 8,6 ns/op appendBounds:·gc.alloc.rate.norm true 5 184,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 10 200,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 50 688,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 100 1192,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 500 5192,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 1000 10200,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 5 136,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 10 160,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 50 360,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 100 608,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 500 2608,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 1000 5104,0 ± 0,0 B/op appendBounds false 5 20,8 ± 0,1 ns/op appendBounds false 10 24,0 ± 0,2 ns/op appendBounds false 50 66,4 ± 0,4 ns/op appendBounds false 100 111,0 ± 0,8 ns/op appendBounds false 500 419,2 ± 2,7 ns/op appendBounds false 1000 840,4 ± 7,8 ns/op appendSubString false 5 25,3 ± 0,3 ns/op appendSubString false 10 25,7 ± 0,2 ns/op appendSubString false 50 36,0 ± 0,1 ns/op appendSubString false 100 52,8 ± 0,4 ns/op appendSubString false 500 206,1 ± 6,1 ns/op appendSubString false 1000 388,1 ± 1,6 ns/op appendBounds:·gc.alloc.rate.norm false 5 80,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 10 88,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 50 320,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 100 544,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 500 2144,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 1000 4152,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 5 96,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 10 112,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 50 192,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 100 288,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 500 1088,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 1000 2088,0 ± 0,0 B/op 

Refutando nuestra suposición, el método appendSubString en la gran mayoría de los casos (incluso siempre para cadenas no latinas) resultó ser más rápido y menos glotón (aunque String::substring devuelve un nuevo objeto). Como sucedio


Miro en el libro, veo un higo


Estudiar el código fuente de StringBuilder ayudará a StringBuilder velo del secreto. Ambos métodos utilizados pasan la llamada a los mismos métodos de AbstractStringBuilder :


 public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, Comparable<StringBuilder>, CharSequence { @Override public StringBuilder append(String str) { super.append(str); return this; } @Override public StringBuilder append(CharSequence s, int start, int end) { super.append(s, start, end); return this; } } 

Vaya a AbstractStringBuilder.append(String) :


 public AbstractStringBuilder append(String str) { if (str == null) { return appendNull(); } int len = str.length(); ensureCapacityInternal(count + len); putStringAt(count, str); count += len; return this; } private final void putStringAt(int index, String str) { if (getCoder() != str.coder()) { inflate(); } str.getBytes(value, index, coder); } 

¿Qué es interesante aquí? El AbstractStringBuilder::inflate , como su nombre lo indica, expande la matriz AbstractStringBuilder.value al combinar cadenas diferentes. Los datos se String::getBytes en el String::getBytes :


 void getBytes(byte[] dst, int dstBegin, byte coder) { if (coder() == coder) { System.arraycopy(value, 0, dst, dstBegin << coder, value.length); } else { // this.coder == LATIN && coder == UTF16 StringLatin1.inflate(value, 0, dst, dstBegin, value.length); } } 

Que es importante Si las cadenas son homogéneas, entonces el System::arraycopy intrínseco System::arraycopy se usa para mover los datos, de lo contrario StringLatin1::inflate , que por delegación nos lleva al StringUTF16::inflate :


 // inflatedCopy byte[] -> byte[] @HotSpotIntrinsicCandidate public static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { // We need a range check here because 'putChar' has no checks checkBoundsOffCount(dstOff, len, dst); for (int i = 0; i < len; i++) { putChar(dst, dstOff++, src[srcOff++] & 0xff); } } @HotSpotIntrinsicCandidate static void putChar(byte[] val, int index, int c) { assert index >= 0 && index < length(val) : "Trusted caller missed bounds check"; index <<= 1; val[index++] = (byte)(c >> HI_BYTE_SHIFT); val[index] = (byte)(c >> LO_BYTE_SHIFT); } 

Por lo tanto, si las filas son homogéneas, entonces el método dependiente de la plataforma System::arraycopy se usa para mover los datos, de lo contrario se usa un bucle (también intrínseco). Esto significa que cuando se pegan dos líneas, donde todos los caracteres están en el conjunto del alfabeto latino principal (en otras palabras, caben en 1 byte), el rendimiento debería ser mucho mejor que cuando se pegan líneas heterogéneas. El punto de referencia confirma esto (ver salida para nonLatin = false ).


Ahora el método AbstractStringBuilder.append(CharSequence, int, int) :


 @Override public AbstractStringBuilder append(CharSequence s, int start, int end) { if (s == null) { s = "null"; } checkRange(start, end, s.length()); int len = end - start; ensureCapacityInternal(count + len); appendChars(s, start, end); return this; } private final void appendChars(CharSequence s, int off, int end) { if (isLatin1()) { byte[] val = this.value; for (int i = off, j = count; i < end; i++) { char c = s.charAt(i); if (StringLatin1.canEncode(c)) { val[j++] = (byte)c; } else { count = j; inflate(); StringUTF16.putCharsSB(this.value, j, s, i, end); count += end - i; return; } } } else { StringUTF16.putCharsSB(this.value, count, s, off, end); } count += end - off; } 

Aquí, el enfoque es similar al del ejemplo anterior: para cadenas homogéneas, se usa un mecanismo más simple (aquí, copia de signos en un bucle), para cadenas heterogéneas usamos StringUTF16 , sin embargo, tenga en cuenta que el StringUTF16::putCharsSB llamado StringUTF16::putCharsSB no StringUTF16::putCharsSB intrinsificado.


 public static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) { checkBoundsBeginEnd(index, index + end - off, val); for (int i = off; i < end; i++) { putChar(val, index++, s.charAt(i)); } } 

Por lo tanto, la estructura interna de ambos métodos y la razón de su desempeño diferente son más o menos claros para nosotros. La pregunta surge naturalmente: ¿qué hacer con el conocimiento adquirido a continuación? Hay varias opciones a la vez:


1) tenga esto en cuenta y cuando detecte un código sospechoso, cámbielo con las manos
2) ve a Tagir y pídele que presente un cheque que hará el trabajo en lugar de nosotros
3) realice cambios en el JDK para que el código no cambie en absoluto.


Por supuesto, comenzamos con el tercero. ¿Listo para arriesgarse?


Abismo


Nosotros entrenaremos en gatos en el código fuente del undécimo Java, puede descargarlo aquí .


La forma más simple y obvia de mejorar es seleccionar una subcadena que ya se encuentre dentro del AbstractStringBuilder.append(CharSequence, int, int) :


 //  public AbstractStringBuilder append(CharSequence s, int start, int end) { if (s == null) { s = "null"; } checkRange(start, end, s.length()); int len = end - start; ensureCapacityInternal(count + len); appendChars(s, start, end); return this; } //  public AbstractStringBuilder append(CharSequence s, int start, int end) { if (s == null) { s = "null"; } checkRange(start, end, s.length()); return append(s.subSequence(start, end).toString()); } 

Ahora necesita construir el JDK, ejecutar las pruebas y ejecutar el benchmark StringBuilderAppendBenchmark::appendBounds en él, cuyos resultados deben compararse con los resultados del mismo benchmark en el JDK original:


 #   before      JDK, # after -   Benchmark nonLatin length before after Units avgt true 5 44,6 64,4 ns/op avgt true 10 45,7 66,3 ns/op avgt true 50 129,0 168,9 ns/op avgt true 100 218,7 281,9 ns/op avgt true 500 907,1 1116,2 ns/op avgt true 1000 1626,4 2002,5 ns/op gc.alloc.rate.norm true 5 184,0 264,0 B/op gc.alloc.rate.norm true 10 200,0 296,0 B/op gc.alloc.rate.norm true 50 688,0 904,0 B/op gc.alloc.rate.norm true 100 1192,0 1552,0 B/op gc.alloc.rate.norm true 500 5192,0 6752,0 B/op gc.alloc.rate.norm true 1000 10200,0 13256,0 B/op avgt false 5 20,8 38,0 ns/op avgt false 10 24,0 37,8 ns/op avgt false 50 66,4 82,9 ns/op avgt false 100 111,0 138,8 ns/op avgt false 500 419,2 531,9 ns/op avgt false 1000 840,4 1002,7 ns/op gc.alloc.rate.norm false 5 80,0 152,0 B/op gc.alloc.rate.norm false 10 88,0 168,0 B/op gc.alloc.rate.norm false 50 320,0 440,0 B/op gc.alloc.rate.norm false 100 544,0 688,0 B/op gc.alloc.rate.norm false 500 2144,0 2688,0 B/op gc.alloc.rate.norm false 1000 4152,0 5192,0 B/op 

Lo que se llama, de repente! No solo no se produjeron mejoras, sino que se produjo un deterioro. Maldición, pero ¿cómo?


El hecho es que al principio, en la descripción del método StringBuilder::append hice una pequeña omisión, pero de importancia crítica. El método fue descrito así:


 public final class StringBuilder { @Override public StringBuilder append(String str) { super.append(str); return this; } } 

Y aquí está su vista completa:


 public final class StringBuilder { @Override @HotSpotIntrinsicCandidate public StringBuilder append(String str) { super.append(str); return this; } } 

El código Java que examinamos anteriormente, que se calienta y compila en el nivel C2, no importa, porque no se ejecuta, sino que es intrínseco. Esto es fácil de probar eliminando el perfil utilizando async-profiler . En lo sucesivo, el perfil se elimina para length = 1000 y nonLatin = true :


 #   `appendSubString`, JDK    ns percent samples top ---------- ------- ------- --- 19096340914 43.57% 1897673 jbyte_disjoint_arraycopy <--------- 13500185356 30.80% 1343343 jshort_disjoint_arraycopy <--------- 4124818581 9.41% 409533 java.lang.String.<init> #   2177311938 4.97% 216375 java.lang.StringUTF16.compress #   1557269661 3.55% 154253 java.util.Arrays.copyOfRange #   349344451 0.80% 34823 appendSubString_avgt_jmhStub 279803769 0.64% 27862 java.lang.StringUTF16.newString 274388920 0.63% 27312 org.openjdk.jmh.infra.Blackhole.consume 160962540 0.37% 15946 SpinPause 122418222 0.28% 11795 __memset_avx2 

El código de StringBuilder (y AbstractStringBuilder ) ni siquiera huele aquí, casi 3/4 del perfil está ocupado por un intrínseco. Me gustaría observar la misma imagen en el perfil de nuestro StringBuilder.append(CharSequence, int, int) "mejorado" StringBuilder.append(CharSequence, int, int) .


De hecho, tenemos esto:


  ns percent samples top ---------- ------- ------- --- 19071221451 43.78% 1897827 jbyte_disjoint_arraycopy 6409223440 14.71% 638348 jlong_disjoint_arraycopy 3933622128 9.03% 387403 java.lang.StringUTF16.newBytesFor 2067248311 4.75% 204193 java.lang.AbstractStringBuilder.ensureCapacityInternal 1929218737 4.43% 194751 java.lang.StringUTF16.compress 1678321343 3.85% 166458 java.util.Arrays.copyOfRange 1621470408 3.72% 160849 java.lang.String.checkIndex 969180099 2.22% 96018 java.util.Arrays.copyOf 581600786 1.34% 57818 java.lang.AbstractStringBuilder.<init> 417818533 0.96% 41611 appendBounds_jmhTest 406565329 0.93% 40479 java.lang.String.<init> 340972882 0.78% 33727 java.lang.AbstractStringBuilder.append 299895915 0.69% 29982 java.lang.StringBuilder.toString 183885595 0.42% 18136 SpinPause 168666033 0.39% 16755 org.openjdk.jmh.infra.Blackhole.consume 

Dirás: "¡Aquí están, intrínsecos, en la cima!" De hecho, solo estos no son los mismos intrínsecos (incl. Compare el nombre del segundo desde arriba). Recordar:


 public final class StringBuilder { @Override @HotSpotIntrinsicCandidate public StringBuilder append(String str) { super.append(str); return this; } } 

Aquí lo intrínseco reemplaza la llamada a StringBuilder.append(String) , pero en nuestro parche esta llamada no lo es. Llamado AbstractStringBuilder.append(String) . La llamada jbyte_disjoint_arraycopy que jbyte_disjoint_arraycopy es intrínseca para StringLatin1::inflate , llamada desde AbstractStringBuider::putStringAt través de String::getBytes . Es decir, a diferencia de StringBuilder::append procesa no solo código de plataforma específico, sino también código Java,


Entendido la causa del fracaso, trate de tener éxito de lo contrario. Es fácil adivinar que necesitamos referirnos de alguna manera a StringBuilder::append . Puede hacerlo arrancando el parche anterior y realizando cambios en el propio StringBuilder :


 public final class StringBuilder { //  @Override public StringBuilder append(CharSequence s, int start, int end) { super.append(s, start, end); return this; } //  @Override public StringBuilder append(CharSequence s, int start, int end) { if (s == null) { s = "null"; } checkRange(start, end, s.length()); return this.append(s.subSequence(start, end).toString()); } } 

Ahora todo se hace sabiamente: se llama al StringBuilder :: append intrínseco.
Reconstruir, ejecutar, comparar:


 #   before      JDK, # after -   Benchmark nonLatin length before after Units avgt true 5 44,6 60,2 ns/op avgt true 10 45,7 59,1 ns/op avgt true 50 129,0 164,6 ns/op avgt true 100 218,7 276,2 ns/op avgt true 500 907,1 1088,8 ns/op avgt true 1000 1626,4 1959,4 ns/op gc.alloc.rate.norm true 5 184,0 264,0 B/op gc.alloc.rate.norm true 10 200,0 296,0 B/op gc.alloc.rate.norm true 50 688,0 904,0 B/op gc.alloc.rate.norm true 100 1192,0 1552,0 B/op gc.alloc.rate.norm true 500 5192,0 6752,0 B/op gc.alloc.rate.norm true 1000 10200,0 13256,0 B/op avgt false 5 20,8 37,9 ns/op avgt false 10 24,0 37,9 ns/op avgt false 50 66,4 80,9 ns/op avgt false 100 111,0 125,6 ns/op avgt false 500 419,2 483,6 ns/op avgt false 1000 840,4 893,8 ns/op gc.alloc.rate.norm false 5 80,0 152,0 B/op gc.alloc.rate.norm false 10 88,0 168,0 B/op gc.alloc.rate.norm false 50 320,0 440,0 B/op gc.alloc.rate.norm false 100 544,0 688,0 B/op gc.alloc.rate.norm false 500 2144,0 2688,0 B/op gc.alloc.rate.norm false 1000 4152,0 5187,2 B/op 

Realmente me siento muy triste, pero no mejoró. Ahora un nuevo perfil:


  ns percent samples top ---------- ------- ------- --- 19614374885 44.12% 1953620 jbyte_disjoint_arraycopy 6645299702 14.95% 662146 jlong_disjoint_arraycopy 4065789919 9.15% 400167 java.lang.StringUTF16.newBytesFor 2374627822 5.34% 234746 java.lang.AbstractStringBuilder.ensureCapacityInternal 1837858014 4.13% 183822 java.lang.StringUTF16.compress 1472039604 3.31% 145956 java.util.Arrays.copyOfRange 1316397864 2.96% 130747 appendBounds_jmhTest 956823151 2.15% 94959 java.util.Arrays.copyOf 573091712 1.29% 56933 java.lang.AbstractStringBuilder.<init> 434454076 0.98% 43202 java.lang.String.<init> 368480388 0.83% 36439 java.lang.AbstractStringBuilder.append 304409057 0.68% 30442 java.lang.StringBuilder.toString 272437989 0.61% 26833 SpinPause 201051696 0.45% 19985 java.lang.StringBuilder.<init> 198934052 0.45% 19810 appendBounds_avgt_jmhStub 

Poco ha cambiado. Para mí, no está claro por qué el intrínseco no funcionó al acceder a StringBuilder.append(String) desde StringBuilder . Existe la sospecha de que pegar (en línea) el cuerpo del método StringBuilder.append(String) en el cuerpo de StringBuilder.append(CharSequence, int, int) cambia algo en el procesamiento de las llamadas al método VM.


De todos modos, este es un fiasco, hermano. No fue posible parchear el JDK, pero aún podemos hacer el reemplazo manualmente donde tenga sentido.


Retiro literario de fracaso
El cifrado de respuesta llegó en dos días. El navegador no quiere separarse de Oto Velara, con una compañía que construye buques de guerra sorprendentemente rápidos y poderosos. El navegador no quiere leerme el cifrado. Simplemente repite la respuesta del puesto de comando: "No." El cifrado no explica por qué "no". "No" en cualquier caso significa que es una persona conocida por una computadora grande. Si no se supiera nada de él, la respuesta sería sí: pruébelo. Que mal. Es una pena perder a una persona tan interesante. Y el comandante debe sentir pena por mí. Quizás la primera vez es una pena. Me ve destrozando a los vikingos. Y no quiere empujarme de nuevo a los galgos.
El calla. Pero sé que al proporcionar una escasez salvaje de trabajadores:
- Yo, camarada general, trabajo mañana para proporcionar. Dejame ir
- Adelante - Y de repente ella sonríe. "Ya sabes, cada nube tiene un lado positivo".
"Yo, camarada general, siempre estoy enfermo sin bien".
"Y aquí está". Se te prohibió conocerlo, eso es malo. Pero a los tesoros de nuestra experiencia, agregamos otro grano.

Conclusiones:


  • El código de los métodos JDK en algunos casos no está relacionado con la ejecución real, porque en lugar del cuerpo del método se puede ejecutar un intrínseco, que está oculto en las entrañas de la VM.
  • tales métodos se pueden reconocer, en particular, la etiqueta @HotSpotIntrinsicCandidate señala, aunque algunos métodos están intrinsificados sin ninguna pista, por ejemplo String::equals (y muchos, muchos otros ).
  • La conclusión que se desprende de los dos primeros es que nuestra discusión sobre cómo funciona el código JDK puede ser contraria a la realidad. C'est la vie

PS
Otro posible reemplazo:


 StringBuilder sb = new StringBuilder(); sb.append(str, 0, endIndex); // --> StringBuilder sb = new StringBuilder(str.substring(o, endIndex)); 

PPS
Los desarrolladores de Oracle señalan con razón que


Me parece bastante extraño y sorprendente introducir una ruta de código en
sb.append (cs, int, int) que asigna memoria para llegar a un intrínseco que
solo a veces hace que las cosas funcionen más rápido. Como observaste, el rendimiento
las compensaciones no son obvias.

En cambio, si queremos optimizar sb.append (cs, int, int) tal vez deberíamos ir
adelante y haga eso, posiblemente agregando o reorganizando los intrínsecos.

La solución propuesta es la intrinsificación de StringBuilder.append(CharSequence, int, int) .


Tarea
Discusión


PPS
Curiosamente, en este momento, al escribir algo como


 StringBuilder sb = new StringBuilder(); sb.append(str.substring(0, endIndex)); 

"Idea" sugiere simplificar el código para


 StringBuilder sb = new StringBuilder(); sb.append(s, 0, endIndex); 

Si el rendimiento en este lugar no es muy importante para usted, probablemente sea más correcto usar la segunda versión simplificada. Aún así, la mayor parte del código que escribimos es para nuestros camaradas, no para las máquinas.

Source: https://habr.com/ru/post/es436746/


All Articles