سلسلة سلسلة ، أو رمز بايت التصحيح

لقد قرأت مؤخرًا مقالة حول تحسين أداء كود Java - على وجه الخصوص ، سلسلة متسلسلة. بقي السؤال فيه - لماذا عند استخدام StringBuilder في الكود تحت القطع ، يعمل البرنامج بشكل أبطأ من الإضافة البسيطة. في هذه الحالة ، + = أثناء التحويل البرمجي يتحول إلى مكالمات StringBuilder.append ().

كان لدي رغبة على الفور في حل المشكلة.

// ~20 000 000    public String stringAppend() { String s = "foo"; s += ", bar"; s += ", baz"; s += ", qux"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; return s; } // ~7 000 000    public String stringAppendBuilder() { StringBuilder sb = new StringBuilder(); sb.append("foo"); sb.append(", bar"); sb.append(", bar"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); return sb.toString(); } 

ثم جاء كل تفكيري إلى حقيقة أن هذا سحر لا يمكن تفسيره داخل JVM ، وقد تخليت عن محاولة إدراك ما كان يحدث. ومع ذلك ، خلال المناقشة التالية للاختلافات بين المنصات في سرعة العمل مع الأوتار ، قررت أنا وصديقي yegorf1 معرفة سبب وكيفية حدوث هذا السحر بالضبط.

Oracle Java SE


تحديث: تم إجراء الاختبارات على Java 8
الحل الواضح هو جمع شفرة المصدر في البايت كود ، ثم النظر في محتوياته. لذا فعلنا. في التعليقات كانت هناك اقتراحات بأن التسارع مرتبط بالتحسين - من الواضح أنه يجب لصق الخطوط الثابتة معًا على مستوى التجميع. اتضح أن الأمر ليس كذلك. فيما يلي جزء من الرمز البايت الذي تم تحويله إلى ملف javap:

  public java.lang.String stringAppend(); Code: 0: ldc #2 // String foo 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: aload_1 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String , bar 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

قد تلاحظ أنه لم يتم إجراء تحسينات. غريب أليس كذلك؟ حسنًا ، دعنا نرى الرمز الفرعي للدالة الثانية.

  public java.lang.String stringAppendBuilder(); Code: 0: new #3 // class java/lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 8: aload_1 9: ldc #2 // String foo 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: pop 15: aload_1 16: ldc #6 // String , bar 18: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

هنا مرة أخرى ، أي تحسينات؟ علاوة على ذلك ، دعنا نلقي نظرة على التعليمات على 8 و 14 و 15 بايت. يحدث شيء غريب هناك - أولاً ، يتم تحميل مرجع إلى كائن من فئة StringBuilder على المكدس ، ثم يتم طرحه من المكدس وإعادة تحميله. يتبادر الحل الأبسط:

  public java.lang.String stringAppendBuilder(); Code: 0: new #41 // class java/lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 8: aload_1 9: ldc #2 // String foo 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String , bar 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

من خلال التخلص من الإرشادات غير الضرورية ، نحصل على رمز يعمل 1.5 مرة أسرع من إصدار stringAppend ، والذي تم فيه تنفيذ هذا التحسين بالفعل. وبالتالي ، فإن خطأ "السحر" هو مترجم البايت كود غير المكتمل ، والذي لا يمكنه إجراء تحسينات بسيطة للغاية.

Android ART


محدث: تم بناء الكود تحت sdk 28 مع أدوات الإصدار
لذلك ، اتضح أن المشكلة تتعلق بتنفيذ مترجم جافا في البايت كود لكومة JVM. هنا تذكرنا وجود ART ، وهو جزء من مشروع Android Open Source Project . تم كتابة هذا الجهاز الظاهري ، أو بالأحرى ، مترجم البايت كود في كود أصلي ، في دعوى قضائية من أوراكل ، والتي تعطينا كل سبب للاعتقاد بأن الاختلافات عن تنفيذ أوراكل كبيرة. بالإضافة إلى ذلك ، نظرًا لخصائص معالجات ARM ، فإن هذا الجهاز الظاهري عبارة عن سجل ، وليس كومة.

دعونا نلقي نظرة على سمالي (أحد تمثيلات البايت كود تحت ART):

 # virtual methods .method public stringAppend()Ljava/lang/String; .registers 4 .prologue .line 6 const-string/jumbo v0, "foo" .line 7 .local v0, "s":Ljava/lang/String; new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 const-string/jumbo v2, ", bar" invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 //... .method public stringAppendBuilder()Ljava/lang/String; .registers 3 .prologue .line 13 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V .line 14 .local v0, "sb":Ljava/lang/StringBuilder; const-string/jumbo v1, "foo" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; .line 15 const-string/jumbo v1, ", bar" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; //... 

في هذه السلسلة المتغيرة AppendBuilder ، لا توجد مشاكل أخرى في المكدس - الجهاز مسجل ، ولا يمكن أن تحدث من حيث المبدأ. ومع ذلك ، لا يتعارض هذا مع وجود أشياء سحرية تمامًا:

 move-result-object v1 

هذه السلسلة في stringAppend لا تفعل شيئًا - الرابط إلى كائن StringBuilder الذي نحتاجه موجود بالفعل في السجل v1. سيكون من المنطقي افتراض أن stringAppend ستعمل بشكل أبطأ. تم تأكيد ذلك تجريبيًا - النتيجة مشابهة لنتيجة النسخة "المصححة" من البرنامج للمكدس JVM: يعمل StringBuilder بشكل أسرع مرة ونصف تقريبًا.

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


All Articles