Bagaimana String.getBytes (UTF_8) saya rusak dan apa yang saya lakukan dengannya

(spoiler) debazed, dibongkar dan sampai pada kesimpulan bahwa masalahnya ada pada instruksi SSE

Halo, 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) { // 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; ... } 

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:

 /* * 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; } 

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:

 /* * 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; } 

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.

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


All Articles