मेरा String.getBytes (UTF_8) कैसे टूट गया और मैंने इसके साथ क्या किया

(स्पॉइलर) डिसेबल्ड, डिसैम्बल्ड और इस निष्कर्ष पर आया कि समस्या एसएसई निर्देशों में है

नमस्कार, हेब्र!

यह सब इस तथ्य से शुरू हुआ कि मैंने जिस सिस्टम पर काम कर रहा हूं उसके आंतरिक घटक के लिए जावा में लोड टेस्ट लिखा। परीक्षण ने कई सूत्र बनाए और बहुत बार कुछ निष्पादित करने की कोशिश की। निष्पादन के दौरान, कभी-कभी java.lang.ArrayIndexOutOfBoundsException: 0 त्रुटियां इस के समान एक रेखा पर दिखाई देती हैं:

"test".getBytes(StandardCharsets.UTF_8) 

बेशक, रेखा अलग थी, लेकिन थोड़ा अध्ययन करने के बाद, मैं इसमें समस्या का पता लगाने में कामयाब रहा। परिणामस्वरूप, JMH बेंचमार्क लिखा गया था:

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

निम्नलिखित अपवाद के साथ ऑपरेशन के कुछ सेकंड बाद दुर्घटनाग्रस्त हो गया:

 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) 

मैंने पहले कभी इसका सामना नहीं किया है, इसलिए मैंने जेवीएम को अपडेट करने और कंप्यूटर को रिबूट करने जैसे तुच्छ समाधानों की कोशिश की, लेकिन यह निश्चित रूप से मदद नहीं करता है। समस्या मेरे मैकबुक प्रो (13-इंच, 2017) 3.5 गीगाहर्ट्ज इंटेल कोर i7 पर उठी और सहकर्मियों की मशीनों पर दोहराई नहीं गई। अन्य कारकों को न पाकर, मैंने आगे कोड का अध्ययन करने का निर्णय लिया।

एन्कोडिंग () विधि में StringCoding वर्ग के JVM के अंदर समस्या हुई:

 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; ... } 

दुर्लभ मामलों में बा सरणी 0 तत्वों की लंबाई के साथ बनाया गया था, और इससे भविष्य में एक त्रुटि हुई।

मैंने UTF_8 पर निर्भरता को हटाने की कोशिश की, लेकिन यह काम नहीं किया। निर्भरता को छोड़ना पड़ा, अन्यथा समस्या पुन: उत्पन्न नहीं हुई, लेकिन यह बहुत सारे अनावश्यक को दूर करने के लिए निकला:

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

maxBytesPerChar 3.0 के बराबर अंतिम फ़ील्ड से एक स्थिरांक लौटाता है, लेकिन दुर्लभ मामलों (1 प्रति 1000000000) में स्वयं विधि 0. वापस आ गई थी। यह दोगुना अजीब था कि सभी मामलों में जिस तरह से काम करना चाहिए था, उस तरीके से काम करने वाले कलाकारों को हटा दिया।

मैंने JIT कंपाइलर विकल्प -XX: -TieredCompilation और -client जोड़ा, लेकिन यह कुछ भी प्रभावित नहीं करता था। अंत में, मैंने मैक के लिए hsdis-amd64.dylib संकलित किया, विकल्प -XX जोड़ा: PrintAssemblyOptions = Intel -XX: CompileCommand = print, * MyBenchmark.encode -XX: CompileCommand = NOTinline, * MyBenchmark.encode डबल और बिना कस्टम के साथ एक विधि के लिए om कोडांतरक:

     : 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 

मतभेदों में से एक vcvtss2sd और vcvttsd2si निर्देशों की उपलब्धता थी। मैंने C ++ पर स्विच किया और इनलाइन asm में सीक्वेंस प्ले करने का फैसला किया, लेकिन डिबगिंग के दौरान यह पता चला कि -O0 विकल्प के साथ क्लैंग कंपाइलर cvtss2sd इंस्ट्रक्शन का उपयोग करता है जब फ्लोट की तुलना की जाती है! = 1.0। अंत में, यह तुलना समारोह में नीचे आया:

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

और दुर्लभ मामलों में यह फ़ंक्शन झूठा लौट आया। मैंने गलत निष्पादन के प्रतिशत की गणना करने के लिए एक छोटा आवरण लिखा है:

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

परिणाम निम्न था: Iterations: 2147483647, त्रुटियां: 111, दूसरी तुलना त्रुटियों: 0. दिलचस्प बात यह है कि दूसरी जांच ने कभी कोई त्रुटि नहीं की।

मैंने क्लैंग के SSE समर्थन को अक्षम कर दिया, तुलनात्मक कार्य इस तरह दिखने लगा:

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

और समस्या अब प्रजनन नहीं थी। इससे मैं यह निष्कर्ष निकाल सकता हूं कि SSE निर्देश सेट मेरे सिस्टम पर बहुत अच्छी तरह से काम नहीं करता है।

मैं 7 से अधिक वर्षों के लिए एक प्रोग्रामर के रूप में काम कर रहा हूं, और मैं 16 से अधिक वर्षों से प्रोग्रामिंग कर रहा हूं, और इस समय के दौरान मुझे आदिम संचालन पर भरोसा करने के लिए उपयोग किया जाता है। यह हमेशा काम करता है और परिणाम हमेशा समान होता है। यह महसूस करने के लिए कि किसी बिंदु पर फ्लोट की तुलना निश्चित रूप से टूट सकती है। और मैक को बदलने के लिए इसके अलावा क्या किया जा सकता है यह स्पष्ट नहीं है।

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


All Articles