(المفسد) محطم ، مفكك وتوصل إلى استنتاج مفاده أن المشكلة تكمن في تعليمات SSEمرحبا يا هبر!
بدأ كل شيء بحقيقة أنني كتبت اختبار تحميل في Java للمكون الداخلي للنظام الذي أعمل عليه. أنشأ الاختبار عدة خيوط وحاول تنفيذ شيء عدة مرات. أثناء التنفيذ ،
أحيانًا 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)
لم يسبق لي أن واجهت هذا من قبل ، لذا جربت حلولًا تافهة مثل تحديث JVM وإعادة تشغيل الكمبيوتر ، ولكن هذا بالطبع لم يساعد. نشأت المشكلة على جهاز MacBook Pro (13 بوصة ، 2017) بسرعة 3.5 غيغاهرتز Intel Core i7 ولم تتكرر على أجهزة الزملاء. لعدم العثور على عوامل أخرى ، قررت دراسة الكود أكثر.
حدثت المشكلة داخل JVM لفئة StringCoding في طريقة encode ():
private static int scale(int len, float expansionFactor) {
تم إنشاء مصفوفة 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 لنظام التشغيل Mac ، وأضفت الخيارات -XX: PrintAssemblyOptions = intel و -XX: CompileCommand = print و * MyBenchmark.encode و -XX: CompileCommand = dontinline و * MyBenchmark.encode وبدأت في مقارنة JIT الذي تم إنشاؤه " مجمع مجمع لطريقة مع العرف في مزدوجة وبدون:
: 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 ، ولكن أثناء التصحيح تبين أن المترجم clang مع الخيار -O0 يستخدم تعليمة cvtss2sd عند مقارنة تعويم! = 1.0. في النهاية ، جاء إلى دالة المقارنة:
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; }
وكانت النتيجة ما يلي: التكرارات: 2147483647 ، الأخطاء: 111 ، مقارنة الأخطاء الثانية: 0. ومن المثير للاهتمام ، أن الاختبار الثاني لم يخطئ أبدًا.
لقد عطلت دعم SSE من clang ، وبدأت وظيفة المقارنة تبدو كما يلي:
bool compare() { float val = 1.0; return val != 1.0; }
ولم تعد المشكلة تتكاثر. من هذا يمكنني أن أستنتج أن مجموعة تعليمات SSE
لا تعمل بشكل جيد على نظامي.
أنا أعمل كمبرمج لأكثر من 7 سنوات ، وكنت أبرمج لأكثر من 16 عامًا ، واعتدت خلال هذا الوقت على الثقة في العمليات البدائية. إنه يعمل دائمًا والنتيجة هي نفسها دائمًا. إن إدراك أن مقارنة تعويم في مرحلة ما قد تنكسر هي بالطبع صدمة. وما يمكن فعله بهذا باستثناء استبدال Mac ليس واضحًا.