كيف ساهمت أداة R8 في تسريع "كوروتينات" Kotlin على Android بمقدار الضعف؟
مدّة القراءة: 7 دقائق
بدءًا من الإصدار 9.2.0 من المكوّن الإضافي لنظام Gradle المتوافق مع Android، يعمل R8 على تحسين معظم طلبات Atomic*FieldUpdater إلى Unsafe variants التي تعمل بأداء أفضل من مرتين إلى 4 مرات في العمليات الشائعة. ويؤثّر ذلك بشكل كبير في مكتبة kotlinx.atomicfu التي تنفّذ عمليات ذرية لـ kotlinx.coroutines، ما يجعل تشغيل وإلغاء الروتينات الفرعية أسرع بمقدار مرتين. للاستفادة من هذه المزايا، عليك تحديث الإصدار الحالي من المكوّن الإضافي Android Gradle إلى الإصدار 9.2.0 أو إصدار أحدث.
مع اعتماد معظم تطبيقات Android للغة Kotlin كلغة أساسية، أصبحت kotlinx.coroutines معيارًا فعليًا للبرمجة غير المتزامنة. توفّر المكتبة طريقة منظَّمة ومصمَّمة جيدًا لإدارة عمليات نقل البيانات المتزامنة، وهي طريقة متوافقة مع لغة Kotlin. ولم يكن Jetpack Compose استثناءً، إذ اعتمد على الروتينات المشتركة لإدارة أحداث المؤشر والصور المتحركة والتفاعلات الأخرى. في وقت كتابة هذا المستند، تستدعي معظم واجهات برمجة التطبيقات المتزامنة في Compose الدوال suspend في الخلفية، وتطلق و/أو تلغي الروتينات المشتركة للتعامل مع عمليات التحديث.
عندما بدأ فريق Compose في التحقيق في الأداء، تبيّن أنّ الكوروتينات تشكّل مؤثِّرًا سلبيًا للعديد من العمليات التي تحدث خارج التكوين. على سبيل المثال، استغرقت عمليات إطلاق وإلغاء الروتينات الفرعية الداخلية التي تعاملت مع تحديثات InteractionSource% 80 من الوقت المستغرَق في إنشاء Modifier.clickable وتعديله. واستنادًا إلى هذه الملاحظات، تركّز الكثير من أعمال الأداء المبكرة على إزالة الروتينات الفرعية من المسار التلقائي وتأخير عملية التهيئة إلى أن تصبح ضرورية.
تكلفة الروتين الفرعي
أسهل طريقة لتحليل السلوك الداخلي لإحدى الدوال على Android هي تسجيل تتبُّع طريقة في "وقت تشغيل Android" (ART). سجلّ إجراءات ART هو أداة تسجّل تسلسل تنفيذ تطبيق، وتوضّح بالتحديد الطرق التي يتم استدعاؤها وترتيبها والوقت المستغرَق في كل طريقة، ما يتيح للمطوّرين تحديد المشاكل التي تؤدي إلى بطء الأداء. بالنسبة إلى طلب LaunchedEffect { } فارغ، سيبدو على النحو التالي:
يمكن تقسيم سجلّ الإجراءات أعلاه إلى ثلاثة أجزاء:
- بدء روتين فرعي جديد
- بدء روتين فرعي
- إكمال الروتين الفرعي (لأنّه يتم إنهاؤه على الفور)
يشبه إلغاء LaunchedEffect الإكمال العادي، باستثناء أنّه يؤدي أيضًا إلى إنشاء CancellationException.
من الملف الشخصي أعلاه، أحد الأشياء التي تثير الشك على الفور هو المكالمات المتكررة إلى java.util.concurrent.AtomicReferenceFieldUpdater (مربّعات أرجوانية أو خضراء تحمل تصنيفات j…). على الرغم من أنّ كل طلب سريع نسبيًا، إلا أنّ معدّل التكرار مثير للقلق، إذ إنّ أي تكلفة إضافية غير ضئيلة يتم توزيعها على عدة عمليات استدعاء قد تؤدي إلى تراجع ملحوظ في الأداء. عند تكبير مكالمة، يتبيّن أنّ معظم الوقت يتم قضاؤه في... عمليات التحقّق من الانعكاس؟
تنفّذ إجراءات coroutine بنية شجرة غير قابلة للإغلاق لعلاقات العنصر الرئيسي والعنصر الثانوي، ما يتيح إمكانية تنفيذ التزامن المنظَّم. تبيّن أنّ مكتبة kotlinx.atomicfu تنفّذ عمليات ذرية غير متزامنة باستخدام عنصر JVM أساسي معروف، وهو AtomicReferenceFieldUpdater. تستخدِم أداة التحديث مرجع فئة واسم حقل لتنفيذ عمليات ذرية في وقت التشغيل، ويجب أن تنفّذ عدة عمليات تحقّق انعكاسية من الأمان للتأكّد من توفّر الحقل وإمكانية الوصول إليه. يؤدي كل إجراء في الروتينات الفرعية (بدء وتعليق وإلغاء وإكمال) إلى استدعاء عملية ذرية واحدة على الأقل، لذا إذا كانت العملية بطيئة، لن تعمل الروتينات الفرعية بشكل جيد.
التحقيق في AtomicReferenceFieldUpdater
ولكن لنستبق الأحداث. AtomicReferenceFieldUpdater في الواقع، تم تحسينها بشكل جيد على JVM لأكثر من 10 سنوات حتى الآن، وقد ترصد عمليات تتبُّع الدوال البرمجية حملًا زائدًا تتم إزالته بالكامل من خلال عملية تحسين على مستوى الجهاز الافتراضي: عمليات تجميع في الوقت المناسب (JIT) أو قبل الوقت (AOT). للتحقّق من الأداء، لنكتب بعض مقاييس الأداء لقياس الفرق بين المراجع الذرية من kotlinx.atomicfu وjava.util.concurrent.atomic.
@RunWith(AndroidJUnit4::class) class AtomicReferenceBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() private val atomicReference = java.util.concurrent.atomic.AtomicReference(false) private val atomicRef = kotlinx.atomicfu.atomic<Boolean>(false) @Test fun atomicReference_compareAndSet() { benchmarkRule.measureRepeated { atomicReference.compareAndSet(true, false) atomicReference.compareAndSet(false, true) } } @Test fun atomicRef_compareAndSet() { benchmarkRule.measureRepeated { atomicRef.compareAndSet(true, false) atomicRef.compareAndSet(false, true) } } /* measuring other methods from the method traces above */ }
عند تشغيل هذا المقياس على هاتف Pixel 5 (مع التأكّد من أنّ AtomicReferenceFieldUpdater#compareAndSet يتم تجميعها في الوقت المناسب أثناء عملية الإحماء)، سيتم الحصول على النتائج التالية على هاتف Pixel 5 (الإصدار 33 من واجهة برمجة التطبيقات):
50.7 ns atomicReference_compareAndSet 135 ns atomicRef_compareAndSet
وتؤكّد القياسات وجود هذا الفارق، إذ إنّ سرعة الإصدار kotlinx.atomicfu أقلّ بنحو 2.7 مرة. يؤكّد ذلك أنّ ART لا تجري أي عمليات تحسين مخفية، وأنّ عمليات التحقّق من إذن الوصول الانعكاسي تضيف عبئًا حقيقيًا أثناء وقت التشغيل.
بالرجوع إلى سجلّ الإجراءات الأصلي، نجد أنّ العمل الوحيد المهم الذي تنفّذه AtomicReferenceFieldUpdater هو الطلب الداخلي إلى Unsafe.getObjectVolatile الذي ينفّذ العملية الذرية الأساسية. في معظم الحالات، يكون برنامج تهيئة أداة التعديل ثابتًا، ويمكن إثبات صحته دائمًا استنادًا إلى بنية الفئة المحيطة. وبالتالي، يمكن تحليل معظم استخدامات AtomicReferenceFieldUpdater بشكل ثابت واستبدالها بمتغير Unsafe داخلي أثناء التجميع. يحدث أيضًا أنّ سلسلة أدوات إنشاء Android تتضمّن برنامجًا مجمّعًا للتحسين خاصًا بها يمكنه تنفيذ ذلك بالضبط.
التحسين باستخدام R8
تتيح فئات Atomic*FieldUpdater استخدامًا دقيقًا وديناميكيًا ومستندًا إلى الانعكاس، ولكنها تُستخدَم غالبًا في أنماط واضحة ثابتة. ويفسّر ذلك كلاً من الأداء الأساسي البطيء والرغبة في التحسين. R8 هو برنامج تجميع وتحسين كامل، وهو مناسب تمامًا لفهم الأنماط الأبسط من أجل تقليل النفقات العامة لعمليات التحقّق من الأمان المستندة إلى الانعكاس. يتلقّى R8 رمز بايت JVM بعد برنامج تجميع Java أو Kotlin، ولكن لتسهيل القراءة، يتم عرض هذه الأمثلة في بنية Java. لهذا السبب، لا تتوفّر وسيطات أنواع للدالة AtomicReferenceFieldUpdater.
class Example { volatile String data = ""; static final AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Example.class, String.class, "data"); void example() { // ... updater.compareAndSet(this, "", "new"); // ... } }
ينشئ المثال الأساسي أداة تعديل نهائية ثابتة تصل إلى حقل متغيّر مع وسيطات ثابتة بسيطة للعنصر الحافظ والنوع واسم الحقل. الانعكاس المستخدَم شفاف تمامًا. من الواضح أنّ أداة التعديل هذه تشير إلى حقل صالح وأنّ الموقع الإلكتروني الذي تم إنشاء أداة التعديل فيه لديه إذن وصول صالح إلى الحقل.
في جوهرها، Atomic*FieldUpdater هي برنامج تضمين حول إزاحة حقل واستدعاءات Unsafe. أفضل سيناريو للتحسين هو استبدال حقل أداة التعديل بحقل إزاحة واستبدال طلبات أداة التعديل بطلبات Unsafe.
تحسين Atomic*FieldUpdater
يتم تنفيذ عملية التحسين على ثلاثة أجزاء: القياس والاستبدال والتنظيف.
قياس حالة التطبيق
الخطوة الأولى هي تقديم حقول الإزاحة إلى جانب حقل أداة التعديل لتسهيل الوصول المباشر من خلال Unsafe call.
static final long updater$offset = SyntheticUnsafe.UNSAFE.objectFieldOffset(Example.class.getDeclaredField("data"))
يتم الوصول إلى الحقل من خلال الانعكاس، ويتم استخدام Unsafe لاستخراج إزاحة الحقل في الفئة. يمثّل هذا الرمز العناصر الداخلية Atomic*FieldUpdater إذا تجاهلت عملية التحقّق من الانعكاس. بدلاً من ذلك، يتم تتبُّع نوع حامل أداة التعديل ونوع الحقل المتغيّر بشكل ثابت في المترجم.
يُرجى العِلم أنّه يتم ترك الحقل الأصلي وعملية تهيئته كما هما، وتعمل عملية التحسين على تسهيل الاستخدامات وتحسينها بشكل متفائل، ثم يتم تنظيفها لاحقًا. هذا أسلوب بسيط للتنفيذ، ولكنه يسمح أيضًا بالتحسين الجزئي لحقول أداة التعديل، حيث يتم ترك بعض الاستخدامات كما هي بينما يتم تحسين البعض الآخر.
الاستبدال
في هذه المرحلة من المترجم، وبعد نقطة ربط مناسبة للتزامن، تتوفّر لدينا قائمة بحقول أداة التعديل التي تمّت إضافة أدوات القياس إليها. وهذا يعني أنّه يمكننا تحسين كل موقع إلكتروني لعرض الإعلانات بشكل فردي استنادًا إلى بعض الشروط. إليك مثالاً على طلب:
updater.compareAndSet(holder, expectedValue, newValue);
الشروط التي تتطلّبها Atomic*FieldUpdater هي:
- هل
updaterتأتي من حقل مزوّد بأدوات؟ أي، هل يمكن للتحليل الثابت تتبُّع قيمة العنصر وصولاً إلى قراءة حقل من أداة تعديل مزوّدة بأدوات؟ - هل
holderهو الفئة نفسها أو فئة فرعية من نوع الحامل المحدّد في الأصل؟ - هل
newValueهو الفئة نفسها أو فئة فرعية من نوع الحقل المحدّد في الأصل؟
في حال استيفاء جميع الشروط، يتم استبدال المكالمة بمكالمة إلى Unsafe بدون أي عمليات فحص انعكاس.
SyntheticUnsafe.UNSAFE.compareAndSwapObject(holder, Example.updater$offset, expectedValue, newValue)
هذه الدالة الجديدة أسرع وأبسط، ولكنّها تختلف عن الدالة الأصلية في ما يتعلّق بطريقة معالجتها للقيم الفارغة في updater وholder. ما لم يتم استبعادها بشكل ثابت، يتم إدراج عمليات التحقّق من القيم الخالية لكليهما.
الإزالة
في هذه المرحلة، يحتوي الصف الحافظ على حقل أداة التعديل الأصلية وحقل الإزاحة الجديد بالإضافة إلى مواقع الاتصال التي قد تستخدم أيًا منهما. إذا لم يتم تحسين أي من المواقع الإلكترونية التي يتم عرض الإعلانات عليها، يجب إزالة حقل الإزاحة، وإذا تم تحسين جميع المواقع الإلكترونية التي يتم عرض الإعلانات عليها، يجب إزالة حقل أداة التعديل. في كلتا الحالتين، يجب أيضًا حذف مكالمة التهيئة. تتم عملية حذف الحقول غير المستخدَمة وإزالة الرموز البرمجية غير النشطة في المترجم، ولكن تتطلب إزالة رمز التهيئة هنا بعض الحيل الإضافية.
قد يكون لكلّ من استدعاء newUpdater وgetDeclaredField آثار جانبية، إذ يمكن أن يؤدّي إلى حدوث استثناءات (كما أنّ طريقة تنفيذهما غير معروفة لأنّها تعتمد على إصدار واجهة برمجة التطبيقات). وهذا يعني أنّه لا يمكن إزالتها بأمان من خلال التحسين العام. لذلك، تطلّب هذا التنظيف مراعاة الحقول التي تمّت مراقبتها بشكلٍ صريح، لأنّه من المعروف بشكلٍ ثابت أنّها لا تحتوي على استثناءات.
في النهاية، يبدو مثال أداة التعديل البسيطة الموضّح أعلاه على النحو التالي بعد التحسين:
النتائج
بعد إجراء عمليات التحسين هذه، يتطابق أداء kotlinx.atomicfu ومعظم الاستخدامات الصريحة AtomicInt/Long/ReferenceFieldUpdater الآن مع AtomicReference عند تطبيق R8. في الواقع، يكون أسرع في بعض مقاييس الأداء، إذ يحتوي kotlinx.atomicfu على مكوّن إضافي لبرنامج التجميع يمكنه تضمين مثيلات atomic في الحقول، ما يقلّل من عمليات التخصيص المطلوبة لإنشاء حقل يتم تعديله بشكل ذري.
كانت Jetpack Compose هي المستفيد الرئيسي من هذا العمل. يحتوي وقت تشغيل Compose على عدد من الاختبارات المعيارية الصغيرة التي تتتبّع أداء الروتينات الفرعية عن كثب لرصد أي تراجع في الأداء في وقت مبكر. عندما تم تعديل مقاييس الأداء إلى إصدار جديد من R8، لاحظنا تحسّنًا بمقدار الضعف عند تشغيل الكوروتينات وإلغائها في LaunchedEffect!
إلى جانب ذلك، يعمل فريق ART على تنفيذ عمليات التحسين هذه بشكلٍ أصلي على مستوى الجهاز الافتراضي. إذا كان تطبيقك يستهدف واجهة برمجة التطبيقات 36 ويعمل على إصدار حديث من Android، من المحتمل أنّ جهازك يحسّن بالفعل إجراءات coroutines بطريقة مشابهة. وقد سجّلت مقاييس الأداء الخاصة بالروتينات الفرعية المذكورة أعلاه تحسّنًا بنسبة% 15 تقريبًا في الأداء بعد تطبيق تحديثات JIT في أحدث إصدارات ART.
سيتم تطبيق هذا التحسين تلقائيًا على تطبيقك عند الترقية إلى الإصدار 9.2.0 من Android Gradle Plugin أو باستخدام الإصدار 9.2.0 من R8 مباشرةً. لمزيد من المعلومات، يُرجى الاطّلاع على أداة D8 لتحويل الرمز البرمجي إلى dex وأداة R8 لتقليل حجم الرمز البرمجي.
-
دراسات الحالةأنشأ الفريق قاعدة رموز برمجية لواجهة مستخدم متوافقة مع الذكاء الاصطناعي أصغر بنسبة% 50 من عملية التنفيذ الأصلية، مع تحقيق انخفاض بنسبة% 35 في وقت تنفيذ وكيل الذكاء الاصطناعي، وانخفاض بنسبة% 32 في عدد عمليات تبادل المعلومات بين المهندس والوكيل، وانخفاض بنسبة% 33 في تكلفة الرموز المميزة.
Pavlo Stavytskyi, Rebecca Franks • 11 دقيقة قراءة -
دراسات الحالةWhatsApp هي أكبر منصة مراسلة في العالم، ويستخدمها مليارات الأشخاص حول العالم. وهي أداة التواصل التلقائية للأشخاص في مختلف المناطق، إذ تربط المستخدمين من خلال الرسائل الخاصة والموثوقة والآمنة.
Niharika Arora, Tracy Agyemang, Mayank Jain • مدّة القراءة: 8 دقائق -
دراسات الحالةمهمتنا في Tinder هي تعزيز التواصل الحقيقي وإلهام المستخدمين من خلال تسهيل اللقاءات وجعلها ممتعة لكل جيل جديد من العازبين.
Ajesh Pai, Ulises Uriel Verduzco Díaz , Tracy Agyemang • مدّة القراءة: 4 دقائق
يمكنك تلقّي أحدث الإحصاءات حول تطوير تطبيقات Android في بريدك الوارد أسبوعيًا.