(spoiler) debazed, dibongkar dan sampai pada kesimpulan bahwa masalahnya ada pada instruksi SSEHalo, Habr!
Semuanya dimulai dengan fakta bahwa saya menulis tes Beban di Jawa untuk komponen internal sistem yang saya kerjakan. Tes menciptakan beberapa utas dan mencoba untuk mengeksekusi sesuatu berkali-kali. Selama eksekusi,
terkadang java.lang.ArrayIndexOutOfBoundsException: 0 kesalahan muncul pada baris yang sangat mirip dengan ini:
"test".getBytes(StandardCharsets.UTF_8)
Garisnya tentu saja berbeda, tetapi setelah sedikit belajar, saya berhasil menemukan masalah di dalamnya. Akibatnya, tolok ukur JMH ditulis:
@Benchmark public byte[] originalTest() { return "test".getBytes(StandardCharsets.UTF_8); }
Yang macet setelah beberapa detik beroperasi dengan pengecualian berikut:
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)
Saya belum pernah mengalami ini sebelumnya, jadi saya mencoba solusi sepele seperti memperbarui JVM dan me-reboot komputer, tetapi ini, tentu saja, tidak membantu. Masalahnya muncul pada MacBook Pro saya (13 inci, 2017) 3,5 GHz Intel Core i7 dan tidak terulang pada mesin rekan. Tidak menemukan faktor lain, saya memutuskan untuk mempelajari kode lebih lanjut.
Masalah terjadi di dalam JVM dari kelas StringCoding dalam metode encode ():
private static int scale(int len, float expansionFactor) {
Array ba dalam kasus yang jarang dibuat dengan panjang 0 elemen, dan ini menyebabkan kesalahan di masa depan.
Saya mencoba menghapus ketergantungan pada UTF_8, tetapi tidak berhasil. Ketergantungan harus dibiarkan, jika tidak masalahnya tidak mereproduksi, tetapi ternyata menghapus banyak yang tidak perlu:
private static int encode() { return (int) ((double) StandardCharsets.UTF_8.newEncoder().maxBytesPerChar()); }
maxBytesPerChar mengembalikan konstanta dari bidang terakhir sama dengan 3.0, tetapi metode itu sendiri dalam kasus yang jarang terjadi (1 per 1000000000) mengembalikan 0. Sangat aneh bahwa menghapus gips dalam gandakan metode yang bekerja sebagaimana mestinya dalam semua kasus.
Saya menambahkan opsi kompiler JIT -XX: -TieredCompilation dan -client tetapi tidak memengaruhi apa pun. Pada akhirnya, saya mengkompilasi hsdis-amd64.dylib untuk Mac, menambahkan opsi -XX: PrintAssemblyOptions = intel, -XX: CompileCommand = print, * MyBenchmark.encode dan -XX: CompileCommand = dontinline, * MyBenchmark.encode dan mulai membandingkan JIT yang dihasilkan assembler om untuk metode dengan kustom ganda dan tanpa:
: 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
Salah satu perbedaannya adalah ketersediaan instruksi vcvtss2sd dan vcvttsd2si. Saya beralih ke C ++ dan memutuskan untuk memainkan urutan dalam inline asm, tetapi selama debugging ternyata kompilator dentang dengan opsi -O0 menggunakan instruksi cvtss2sd ketika membandingkan float! = 1.0. Pada akhirnya, ia turun ke fungsi bandingkan:
bool compare() { float val = 1.0; return val != 1.0; }
Dan fungsi ini dalam kasus yang jarang dikembalikan palsu. Saya menulis pembungkus kecil untuk menghitung persentase eksekusi yang salah:
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; }
Hasilnya adalah sebagai berikut: Iterasi: 2147483647, kesalahan: 111, kesalahan membandingkan kedua: 0. Menariknya, pemeriksaan kedua tidak pernah melempar kesalahan.
Saya menonaktifkan dukungan SSE dentang, fungsi membandingkan mulai terlihat seperti ini:
bool compare() { float val = 1.0; return val != 1.0; }
Dan masalahnya tidak lagi mereproduksi. Dari sini saya dapat menyimpulkan bahwa set instruksi SSE
tidak berfungsi dengan baik pada sistem saya.
Saya telah bekerja sebagai programmer selama lebih dari 7 tahun, dan saya telah pemrograman selama lebih dari 16 tahun, dan selama ini saya terbiasa mempercayai operasi primitif. Selalu berhasil dan hasilnya selalu sama. Untuk menyadari bahwa perbandingan float di beberapa titik mungkin rusak tentu saja mengejutkan. Dan apa yang bisa dilakukan dengan ini kecuali mengganti Mac tidak jelas.