لقد قرأت مؤخرًا
مقالة حول تحسين أداء كود Java - على وجه الخصوص ، سلسلة متسلسلة. بقي السؤال فيه - لماذا عند استخدام StringBuilder في الكود تحت القطع ، يعمل البرنامج بشكل أبطأ من الإضافة البسيطة. في هذه الحالة ، + = أثناء التحويل البرمجي يتحول إلى مكالمات StringBuilder.append ().
كان لدي رغبة على الفور في حل المشكلة.
ثم جاء كل تفكيري إلى حقيقة أن هذا سحر لا يمكن تفسيره داخل JVM ، وقد تخليت عن محاولة إدراك ما كان يحدث. ومع ذلك ، خلال المناقشة التالية للاختلافات بين المنصات في سرعة العمل مع الأوتار ، قررت
أنا وصديقي
yegorf1 معرفة سبب وكيفية حدوث هذا السحر بالضبط.
Oracle Java SE
تحديث: تم إجراء الاختبارات على Java 8الحل الواضح هو جمع شفرة المصدر في البايت كود ، ثم النظر في محتوياته. لذا فعلنا. في التعليقات
كانت هناك اقتراحات بأن التسارع مرتبط بالتحسين - من الواضح أنه يجب لصق الخطوط الثابتة معًا على مستوى التجميع. اتضح أن الأمر ليس كذلك. فيما يلي جزء من الرمز البايت الذي تم تحويله إلى ملف javap:
public java.lang.String stringAppend(); Code: 0: ldc #2
قد تلاحظ أنه لم يتم إجراء تحسينات. غريب أليس كذلك؟ حسنًا ، دعنا نرى الرمز الفرعي للدالة الثانية.
public java.lang.String stringAppendBuilder(); Code: 0: new #3
هنا مرة أخرى ، أي تحسينات؟ علاوة على ذلك ، دعنا نلقي نظرة على التعليمات على 8 و 14 و 15 بايت. يحدث شيء غريب هناك - أولاً ، يتم تحميل مرجع إلى كائن من فئة StringBuilder على المكدس ، ثم يتم طرحه من المكدس وإعادة تحميله. يتبادر الحل الأبسط:
public java.lang.String stringAppendBuilder(); Code: 0: new #41
من خلال التخلص من الإرشادات غير الضرورية ، نحصل على رمز يعمل 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
في هذه السلسلة المتغيرة AppendBuilder ، لا توجد مشاكل أخرى في المكدس - الجهاز مسجل ، ولا يمكن أن تحدث من حيث المبدأ. ومع ذلك ، لا يتعارض هذا مع وجود أشياء سحرية تمامًا:
move-result-object v1
هذه السلسلة في stringAppend لا تفعل شيئًا - الرابط إلى كائن StringBuilder الذي نحتاجه موجود بالفعل في السجل v1. سيكون من المنطقي افتراض أن stringAppend ستعمل بشكل أبطأ. تم تأكيد ذلك تجريبيًا - النتيجة مشابهة لنتيجة النسخة "المصححة" من البرنامج للمكدس JVM: يعمل StringBuilder بشكل أسرع مرة ونصف تقريبًا.