كيف جعلت أداة R8 أنماط "كوروتين" في Kotlin على Android أسرع بمرّتين
مدّة القراءة: 7 دقائق
بدءًا من الإصدار 9.2.0 من AGP، يحسِّن R8 معظم طلبات Atomic*FieldUpdater إلى صيغ Unsafe التي تحقّق أداءً أفضل بمقدار مرتين إلى 4 مرات في العمليات الشائعة. ويؤثر ذلك بشكل كبير في مكتبة kotlinx.atomicfu التي تنفّذ العمليات الذرية في kotlinx.coroutines، ما يجعل تشغيل وإلغاء الروتينات المساعدة أسرع بمقدار مرتين. للاستفادة من هذه المزايا، يُرجى تحديث AGP إلى الإصدار 9.2.0 أو إصدار أحدث.
مع اعتماد غالبية تطبيقات Android على لغة Kotlin كلغة رئيسية، أصبحت kotlinx.coroutines معيارًا فعليًا للبرمجة غير المتزامنة. وتوفّر المكتبة طريقة جيدة التصميم ومنظَّمة لإدارة التدفقات المتزامنة المضمّنة في Kotlin. ولم يكن Jetpack Compose استثناءً، إذ اعتمد على الكوروتين لإدارة أحداث المؤشرات والصور المتحركة والتفاعلات الأخرى. في وقت كتابة هذه المقالة، تستدعي معظم واجهات برمجة التطبيقات المتزامنة في Compose دوال suspend في الخلفية وتطلق و/أو تلغي الكوروتين للتعامل مع التحديثات.
عندما بدأ فريق Compose في التحقيق في الأداء، تم اكتشاف أنّ الكوروتين يمثّل عقبة أمام العديد من العمليات التي تحدث خارج عملية الإنشاء. على سبيل المثال، تم استهلاك% 80 من الوقت المستغرَق في إنشاء وتعديل Modifier.clickable من خلال تشغيل وإلغاء وظائف coroutine الداخلية التي عالجت تحديثات InteractionSource. بناءً على هذه الملاحظات، ركّز جزء كبير من العمل المبكر على الأداء على إزالة الكوروتين من المسار التلقائي وتأخير الإعداد حتى يصبح ضروريًا.
تكلفة الكوروتين
أسهل طريقة لتحليل السلوك الداخلي لدالة على Android هي تسجيل تتبُّع إجراءات وقت تشغيل Android (ART). سجلّ الإجراءات في وقت تشغيل Android (ART) هو أداة تسجّل مسار تنفيذ التطبيق، وتوضّح بدقة الإجراءات التي يتم استدعاؤها وترتيبها والوقت المستغرَق في كل منها، ما يتيح للمطوّرين تحديد المشاكل التي تؤثر في الأداء. بالنسبة إلى طلب LaunchedEffect { } فارغ، سيبدو على النحو التالي:
يمكن تقسيم سجلّ الإجراءات أعلاه إلى ثلاثة أجزاء:
- جارٍ بدء دالة coroutine جديدة
- جارٍ بدء الكوروتين
- إكمال الكوروتين (لأنّه يخرج على الفور)
يشبه الإلغاء LaunchedEffect الإكمال العادي، باستثناء أنّه ينشئ أيضًا CancellationException.
من الملف الشخصي أعلاه، هناك أمر واحد مريب على الفور وهو المكالمات المتكررة إلى java.util.concurrent.AtomicReferenceFieldUpdater (المربّعات الأرجوانية أو الخضراء التي تحمل تصنيفات j…). على الرغم من أنّ كل استدعاء سريع نسبيًا، فإنّ معدّل التكرار يثير القلق، فأي عبء غير مهمل يتم توزيعه على عدة استدعاءات قد يؤدي إلى تراجع ملحوظ في الأداء. عند تكبير الصورة في مكالمة، يتبيّن أنّ معظم الوقت يُقضى في... التحقّق من الانعكاسات؟
تنفّذ الكوروتين بنية شجرية خالية من القفل لعلاقات العناصر الرئيسية والثانوية التي تجعل التزامن المنظَّم ممكنًا. اتّضح أنّ مكتبة kotlinx.atomicfu تنفّذ عمليات ذرية بدون قفل باستخدام نوع أساسي معروف في جهاز Java الافتراضي (JVM)، وهو AtomicReferenceFieldUpdater. تستخدم أداة التحديث مرجع فئة واسم حقل لتنفيذ عمليات ذرية في وقت التشغيل، وعليها إجراء عدة عمليات تحقّق من الأمان الانعكاسي للتأكّد من توفّر الحقل وإمكانية الوصول إليه. تستدعي كل عملية في الروتينات المساعدة (البدء والتعليق والإلغاء والإكمال) عملية ذرية واحدة على الأقل، لذا إذا كانت بطيئة، فلن تعمل الروتينات المساعدة بشكل جيد.
التحقيق في AtomicReferenceFieldUpdater
ولكن دعونا لا نستبق الأحداث. يتم تحسين AtomicReferenceFieldUpdater بشكل جيد على جهاز Java الافتراضي (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 مرة تقريبًا. يؤكد هذا أنّ وقت تشغيل Android لا يُجري أي تحسين مخفي، وأنّ عمليات التحقّق من الوصول الانعكاسي تضيف عبئًا حقيقيًا أثناء وقت التشغيل.
بالرجوع إلى سجلّ الإجراءات الأصلي، نجد أنّ الإجراء الوحيد المفيد الذي نفّذته 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، من المحتمل أن يكون جهازك يحسّن أداء الكوروتين بطريقة مشابهة. لاحظت مقاييس أداء الكوروتين أعلاه تحسُّنًا بنسبة% 15 تقريبًا في الأداء بعد تحديثات JIT في الإصدارات الأخيرة من ART.
سيتم تطبيق هذا التحسين على تطبيقك تلقائيًا عند الترقية إلى الإصدار 9.2.0 من AGP أو باستخدام الإصدار 9.2.0 من R8 مباشرةً. لمزيد من المعلومات، يُرجى الاطّلاع على أداة D8 dexer وأداة R8 shrinker.
-
دراسات الحالةGrab هو التطبيق الفائق الرائد في جنوب شرق آسيا، ويقدّم خدمات التوصيل والتنقّل والخدمات المالية لملايين المستخدمين في ثمانية بلدان من خلال تطبيق واحد.
Ajesh Pai, Nguyen Van Minh, Tracy Agyemang • مدّة القراءة: 5 دقائق -
دراسات الحالةأنشأ الفريق قاعدة رموز برمجية لواجهة مستخدم مستندة إلى الذكاء الاصطناعي أصغر بنسبة% 50 من عملية التنفيذ الأصلية، مع تحقيق انخفاض بنسبة% 35 في وقت تنفيذ وكيل الذكاء الاصطناعي، وانخفاض بنسبة% 32 في عمليات التبادل بين المهندسين والوكلاء، وانخفاض بنسبة% 33 في تكلفة الرموز المميّزة.
Pavlo Stavytskyi, Rebecca Franks • مدّة القراءة: 11 دقيقة -
دراسات الحالةWhatsApp هو أكبر منصة مراسلة في العالم، ويستخدمه مليارات الأشخاص على مستوى العالم. وهو أداة التواصل التلقائية للمستخدمين في مختلف المناطق، إذ يربط بينهم من خلال رسائل خاصة وموثوقة وآمنة.
Niharika Arora, Tracy Agyemang, Mayank Jain • مدّة القراءة: 8 دقائق
يمكنك تلقّي آخر الإحصاءات حول تطوير تطبيقات Android في بريدك الوارد أسبوعيًا.