Wie meine String.getBytes (UTF_8) kaputt gingen und was ich damit gemacht habe

(Spoiler) entkräftet, zerlegt und kam zu dem Schluss, dass das Problem in SSE-Anweisungen liegt

Hallo Habr!

Alles begann damit, dass ich einen Lasttest in Java für die interne Komponente des Systems geschrieben habe, an dem ich arbeite. Der Test hat mehrere Threads erstellt und sehr oft versucht, etwas auszuführen. Während der Ausführung treten manchmal java.lang.ArrayIndexOutOfBoundsException: 0 Fehler in einer Zeile auf, die dieser sehr ähnlich ist:

"test".getBytes(StandardCharsets.UTF_8) 

Die Linie war natürlich anders, aber nach einer kleinen Studie gelang es mir, das Problem darin zu finden. Als Ergebnis wurde der JMH-Benchmark geschrieben:

 @Benchmark public byte[] originalTest() { return "test".getBytes(StandardCharsets.UTF_8); } 

Was nach ein paar Sekunden Betrieb mit der folgenden Ausnahme abstürzte:

 java.lang.ArrayIndexOutOfBoundsException: 0 at sun.nio.cs.UTF_8$Encoder.encode(UTF_8.java:716) at java.lang.StringCoding.encode(StringCoding.java:364) at java.lang.String.getBytes(String.java:941) at org.sample.MyBenchmark.originalTest(MyBenchmark.java:41) at org.sample.generated.MyBenchmark_originalTest.originalTest_thrpt_jmhLoop(MyBenchmark_originalTest.java:103) at org.sample.generated.MyBenchmark_originalTest.originalTest_Throughput(MyBenchmark_originalTest.java:72) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.openjdk.jmh.runner.LoopBenchmarkHandler$BenchmarkTask.call(LoopBenchmarkHandler.java:210) at org.openjdk.jmh.runner.LoopBenchmarkHandler$BenchmarkTask.call(LoopBenchmarkHandler.java:192) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) 

Ich habe dies noch nie zuvor erlebt, daher habe ich triviale Lösungen wie das Aktualisieren der JVM und das Neustarten des Computers ausprobiert, aber dies hat natürlich nicht geholfen. Das Problem trat bei meinem MacBook Pro (13 Zoll, 2017) mit 3,5 GHz Intel Core i7 auf und wiederholte sich nicht auf den Computern der Kollegen. Da ich keine anderen Faktoren fand, beschloss ich, den Code weiter zu studieren.

Das Problem trat in der JVM der StringCoding-Klasse in der encode () -Methode auf:

 private static int scale(int len, float expansionFactor) { // We need to perform double, not float, arithmetic; otherwise // we lose low order bits when len is larger than 2**24. return (int)(len * (double)expansionFactor); } static byte[] encode(Charset cs, char[] ca, int off, int len) { CharsetEncoder ce = cs.newEncoder(); int en = scale(len, ce.maxBytesPerChar()); byte[] ba = new byte[en]; if (len == 0) return ba; ... } 

Das ba-Array wurde in seltenen Fällen mit einer Länge von 0 Elementen erstellt, was in Zukunft zu einem Fehler führte.

Ich habe versucht, die Abhängigkeit von UTF_8 zu entfernen, aber es hat nicht funktioniert. Die Abhängigkeit musste verlassen werden, sonst reproduzierte sich das Problem nicht, aber es stellte sich heraus, dass viele unnötige entfernt wurden:

 private static int encode() { return (int) ((double) StandardCharsets.UTF_8.newEncoder().maxBytesPerChar()); } 

maxBytesPerChar gibt eine Konstante aus dem letzten Feld zurück, die gleich 3,0 ist, aber die Methode selbst hat in seltenen Fällen (1 pro 1000000000) 0 zurückgegeben. Es war doppelt seltsam, dass das Entfernen des Cast in der doppelten Methode in allen Fällen so funktionierte, wie es sollte.

Ich habe die JIT-Compileroptionen -XX: -TieredCompilation und -client hinzugefügt, aber es hat nichts beeinflusst. Am Ende habe ich hsdis-amd64.dylib für Mac kompiliert, die Optionen -XX: PrintAssemblyOptions = intel, -XX: CompileCommand = print, * MyBenchmark.encode und -XX: CompileCommand = dontinline, * MyBenchmark.encode hinzugefügt und mit dem Vergleich der generierten JIT begonnen. om Assembler für eine Methode mit benutzerdefinierten in double und ohne:

     : 0x000000010a44e3ca: mov rbp,rax ;*synchronization entry ; - sun.nio.cs.UTF_8$Encoder::<init>@-1 (line 558) ; - sun.nio.cs.UTF_8$Encoder::<init>@2 (line 554) ; - sun.nio.cs.UTF_8::newEncoder@6 (line 72) ; - org.sample.MyBenchmark::encode@3 (line 50) 0x000000010a44e3cd: movabs rdx,0x76ab16350 ; {oop(a 'sun/nio/cs/UTF_8')} 0x000000010a44e3d7: vmovss xmm0,DWORD PTR [rip+0xffffffffffffff61] # 0x000000010a44e340 ; {section_word} 0x000000010a44e3df: vmovss xmm1,DWORD PTR [rip+0xffffffffffffff5d] # 0x000000010a44e344 ; {section_word} 0x000000010a44e3e7: mov rsi,rbp 0x000000010a44e3ea: nop 0x000000010a44e3eb: call 0x000000010a3f40a0 ; OopMap{rbp=Oop off=144} ;*invokespecial <init> ; - sun.nio.cs.UTF_8$Encoder::<init>@6 (line 558) ; - sun.nio.cs.UTF_8$Encoder::<init>@2 (line 554) ; - sun.nio.cs.UTF_8::newEncoder@6 (line 72) ; - org.sample.MyBenchmark::encode@3 (line 50) ; {optimized virtual_call} 0x000000010a44e3f0: mov BYTE PTR [rbp+0x2c],0x3f ;*new ; - sun.nio.cs.UTF_8::newEncoder@0 (line 72) ; - org.sample.MyBenchmark::encode@3 (line 50) 0x000000010a44e3f4: vcvtss2sd xmm0,xmm0,DWORD PTR [rbp+0x10] 0x000000010a44e3f9: vcvttsd2si eax,xmm0 0x000000010a44e3fd: cmp eax,0x80000000 0x000000010a44e403: jne 0x000000010a44e414 0x000000010a44e405: sub rsp,0x8 0x000000010a44e409: vmovsd QWORD PTR [rsp],xmm0 0x000000010a44e40e: call Stub::d2i_fixup ; {runtime_call} 0x000000010a44e413: pop rax ;*d2i ; - org.sample.MyBenchmark::encode@10 (line 50) 0x000000010a44e414: add rsp,0x20 0x000000010a44e418: pop rbp    : 0x000000010ef7e04a: mov rbp,rax ;*synchronization entry ; - sun.nio.cs.UTF_8$Encoder::<init>@-1 (line 558) ; - sun.nio.cs.UTF_8$Encoder::<init>@2 (line 554) ; - sun.nio.cs.UTF_8::newEncoder@6 (line 72) ; - org.sample.MyBenchmark::encode@3 (line 50) 0x000000010ef7e04d: movabs rdx,0x76ab16350 ; {oop(a 'sun/nio/cs/UTF_8')} 0x000000010ef7e057: vmovss xmm0,DWORD PTR [rip+0xffffffffffffff61] # 0x000000010ef7dfc0 ; {section_word} 0x000000010ef7e05f: vmovss xmm1,DWORD PTR [rip+0xffffffffffffff5d] # 0x000000010ef7dfc4 ; {section_word} 0x000000010ef7e067: mov rsi,rbp 0x000000010ef7e06a: nop 0x000000010ef7e06b: call 0x000000010ef270a0 ; OopMap{rbp=Oop off=144} ;*invokespecial <init> ; - sun.nio.cs.UTF_8$Encoder::<init>@6 (line 558) ; - sun.nio.cs.UTF_8$Encoder::<init>@2 (line 554) ; - sun.nio.cs.UTF_8::newEncoder@6 (line 72) ; - org.sample.MyBenchmark::encode@3 (line 50) ; {optimized virtual_call} 0x000000010ef7e070: mov BYTE PTR [rbp+0x2c],0x3f ;*new ; - sun.nio.cs.UTF_8::newEncoder@0 (line 72) ; - org.sample.MyBenchmark::encode@3 (line 50) 0x000000010ef7e074: vmovss xmm1,DWORD PTR [rbp+0x10] 0x000000010ef7e079: vcvttss2si eax,xmm1 0x000000010ef7e07d: cmp eax,0x80000000 0x000000010ef7e083: jne 0x000000010ef7e094 0x000000010ef7e085: sub rsp,0x8 0x000000010ef7e089: vmovss DWORD PTR [rsp],xmm1 0x000000010ef7e08e: call Stub::f2i_fixup ; {runtime_call} 0x000000010ef7e093: pop rax ;*f2i ; - org.sample.MyBenchmark::encode@9 (line 50) 0x000000010ef7e094: add rsp,0x20 0x000000010ef7e098: pop rbp 

Einer der Unterschiede war die Verfügbarkeit der Anweisungen vcvtss2sd und vcvttsd2si. Ich wechselte zu C ++ und entschied mich, die Sequenz in Inline-ASM abzuspielen, aber beim Debuggen stellte sich heraus, dass der Clang-Compiler mit der Option -O0 beim Vergleich von float! = 1.0 die Anweisung cvtss2sd verwendet. Am Ende kam es auf die Vergleichsfunktion an:

 /* * sse 0x105ea2f30 <+0>: pushq %rbp 0x105ea2f31 <+1>: movq %rsp, %rbp 0x105ea2f34 <+4>: movsd 0x6c(%rip), %xmm0 ; xmm0 = mem[0],zero 0x105ea2f3c <+12>: movss 0x6c(%rip), %xmm1 ; xmm1 = mem[0],zero,zero,zero 0x105ea2f44 <+20>: movss %xmm1, -0x4(%rbp) -> 0x105ea2f49 <+25>: cvtss2sd -0x4(%rbp), %xmm1 0x105ea2f4e <+30>: ucomisd %xmm0, %xmm1 0x105ea2f52 <+34>: setne %al 0x105ea2f55 <+37>: setp %cl 0x105ea2f58 <+40>: orb %cl, %al 0x105ea2f5a <+42>: andb $0x1, %al 0x105ea2f5c <+44>: movzbl %al, %eax 0x105ea2f5f <+47>: popq %rbp 0x105ea2f60 <+48>: retq 0x105ea2f61 <+49>: nopw %cs:(%rax,%rax) */ bool compare() { float val = 1.0; return val != 1.0; } 

Und diese Funktion hat in seltenen Fällen false zurückgegeben. Ich habe einen kleinen Wrapper geschrieben, um den Prozentsatz fehlerhafter Ausführungen zu berechnen:

 int main() { int error = 0; int secondCompareError = 0; for (int i = 0; i < INT_MAX; i++) { float result = 1.0; if (result != 1.0) { error++; if (result != 1.0) { secondCompareError++; } } } std::cout << "Iterations: " << INT_MAX << ", errors: " << error <<", second compare errors: " << secondCompareError << std::endl; return 0; } 

Das Ergebnis war das Folgende: Iterationen: 2147483647, Fehler: 111, zweite Vergleichsfehler: 0. Interessanterweise hat die zweite Prüfung nie einen Fehler ausgelöst.

Ich habe die SSE-Unterstützung von clang deaktiviert. Die Vergleichsfunktion sah folgendermaßen aus:

 /* * no sse 0x102745f50 <+0>: pushq %rbp 0x102745f51 <+1>: movq %rsp, %rbp 0x102745f54 <+4>: movl $0x3f800000, -0x4(%rbp) ; imm = 0x3F800000 -> 0x102745f5b <+11>: flds -0x4(%rbp) 0x102745f5e <+14>: fld1 0x102745f60 <+16>: fxch %st(1) 0x102745f62 <+18>: fucompi %st(1) 0x102745f64 <+20>: fstp %st(0) 0x102745f66 <+22>: setp %al 0x102745f69 <+25>: setne %cl 0x102745f6c <+28>: orb %al, %cl 0x102745f6e <+30>: andb $0x1, %cl 0x102745f71 <+33>: movzbl %cl, %eax 0x102745f74 <+36>: popq %rbp 0x102745f75 <+37>: retq 0x102745f76 <+38>: nopw %cs:(%rax,%rax) */ bool compare() { float val = 1.0; return val != 1.0; } 

Und das Problem reproduzierte sich nicht mehr. Daraus kann ich schließen, dass der SSE-Befehlssatz auf meinem System nicht sehr gut funktioniert.

Ich arbeite seit mehr als 7 Jahren als Programmierer, und ich programmiere seit mehr als 16 Jahren. Während dieser Zeit bin ich es gewohnt, primitiven Operationen zu vertrauen. Es funktioniert immer und das Ergebnis ist immer das gleiche. Zu erkennen, dass ein Vergleich eines Schwimmers irgendwann brechen kann, ist natürlich ein Schock. Und was damit getan werden kann, außer den Mac zu ersetzen, ist nicht klar.

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


All Articles