كيفية استخدام 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 وتعديله في تشغيل وإلغاء الكوروتين الداخلي الذي تعامل مع تعديلات 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 الطلب.
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.
-
دراسات الحالةتتطلّب تطبيقات إثبات الهوية للخدمات المصرفية وتسجيل شريحة SIM التقاط صور واضحة وعالية الدقة للوجه من أجل رصد عمليات الانتحال والتزييف العميق. يواجه مطوّرو تطبيقات الكاميرا على Android تحديًا شائعًا يتمثل في التقاط لقطات واضحة بما يكفي لإجراء عمليات التحقّق من الحيوية ومكافحة الانتحال، بدون أن يؤدي ذلك إلى تعطُّل الأجهزة ذات الذاكرة المنخفضة
Donovan McMurray • مدّة القراءة: 4 دقائق -
دراسات الحالةGrab هو التطبيق الفائق الرائد في جنوب شرق آسيا، ويقدّم خدمات التوصيل والتنقّل والخدمات المالية لملايين المستخدمين في ثمانية بلدان من خلال تطبيق واحد.
Ajesh Pai, Nguyen Van Minh, Tracy Agyemang • مدّة القراءة: 5 دقائق -
دراسات الحالةأنشأ الفريق قاعدة رموز برمجية لواجهة مستخدم مستندة إلى الذكاء الاصطناعي أصغر بنسبة %50 من عملية التنفيذ الأصلية، مع تحقيق انخفاض بنسبة %35 في وقت تنفيذ وكيل الذكاء الاصطناعي، وانخفاض بنسبة %32 في عمليات التبادل بين المهندسين والوكلاء، وانخفاض بنسبة %33 في تكلفة الرموز المميّزة.
Pavlo Stavytskyi, Rebecca Franks • مدّة القراءة: 11 دقيقة
يمكنك الحصول على آخر الإحصاءات حول تطوير تطبيقات Android في بريدك الوارد أسبوعيًا.