(Spoiler) entkräftet, zerlegt und kam zu dem Schluss, dass das Problem in SSE-Anweisungen liegtHallo 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) {
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:
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:
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.