كيف ساهمت أداة R8 في تسريع أنماط "كوروتين" في Kotlin على Android بمقدار الضعف
قراءة لمدة 7 دقائق
بدءًا من الإصدار 9.2.0 من المكوّن الإضافي لنظام Gradle المتوافق مع Android (AGP)، تحسِّن أداة R8 معظم طلبات Atomic*FieldUpdater إلى متغيرات غير آمنة تحقّق أداءً أفضل من الضعف إلى أربعة أضعاف في العمليات الشائعة. ويؤثر ذلك بشكل كبير في مكتبة kotlinx.atomicfu التي تنفّذ العمليات الذرية لـ kotlinx.coroutines، ما يجعل تشغيل أنماط "كوروتين" وإلغاءها أسرع بمقدار الضعف. للاستفادة من هذه الميزة، عليك تعديل المكوّن الإضافي لنظام Gradle المتوافق مع Android (AGP) إلى الإصدار 9.2.0 أو إصدار أعلى.
مع استخدام غالبية تطبيقات Android للغة Kotlin كلغة رئيسية، أصبحت kotlinx.coroutines معيارًا فعليًا للبرمجة غير المتزامنة. تقدّم المكتبة طريقة مصمّمة ومنظّمة جيدًا لإدارة التدفقات المتزامنة التي تتوافق مع لغة Kotlin. لم يكن Jetpack Compose استثناءً، فقد استخدم أنماط "كوروتين" لإدارة أحداث المؤشر والصور المتحركة والتفاعلات الأخرى. في وقت كتابة هذا المقال، تستدعي معظم واجهات برمجة التطبيقات المتزامنة في Compose وظائف suspend في الخلفية، وتُشغّل أنماط "كوروتين" و/أو تلغيها للتعامل مع التحديثات.
عندما بدأ فريق Compose في التحقيق في الأداء، تبيّن أنّ أنماط "كوروتين" تمثّل مؤثِّرًا سلبيًا للعديد من العمليات التي تحدث خارج التكوين. على سبيل المثال، استغرقت عملية إنشاء Modifier.clickable وتعديلها 80% من الوقت في تشغيل أنماط "كوروتين" الداخلية وإلغائها، والتي كانت تتعامل مع تحديثات InteractionSource. استنادًا إلى هذه الملاحظات، ركّز جزء كبير من العمل المبكر على الأداء على إزالة أنماط "كوروتين" من المسار التلقائي وتأخير عملية التهيئة إلى أن تصبح ضرورية.
تكلفة نمط "كوروتين"
أسهل طريقة لتحليل السلوك الداخلي لوظيفة على Android هي تسجيل سجلّ الإجراءات في وقت تشغيل Android (ART). سجلّ إجراءات ART هو أداة تسجِّل مسار تنفيذ التطبيق، وتوضّح الطرق التي يتم استدعاؤها بالضبط وترتيبها والوقت الذي يتم استغراقه في كل طريقة، ما يسمح للمطوّرين بتحديد مؤثِّرات الأداء السلبية. بالنسبة إلى طلب فارغ LaunchedEffect { }، سيبدو على النحو التالي:
يمكن تقسيم سجلّ الإجراءات أعلاه إلى ثلاثة أجزاء:
- تهيئة نمط "كوروتين" جديد
- بدء نمط "كوروتين"
- إكمال نمط "كوروتين" (لأنّه يخرج على الفور)
يشبه إلغاء LaunchedEffect عملية الإكمال العادية، باستثناء أنّه ينشئ أيضًا CancellationException.
من الملف الشخصي أعلاه، هناك أمر مثير للشك على الفور وهو عمليات الاستدعاء المتكررة لـ java.util.concurrent.AtomicReferenceFieldUpdater (مربّعات أرجوانية أو خضراء تحمل تصنيفات j…). على الرغم من أنّ كل عملية استدعاء سريعة نسبيًا، فإنّ التكرار يثير القلق، إذ إنّ أي تكلفة غير ضئيلة يتم توزيعها على عمليات استدعاء متعددة قد تؤدي إلى تراجع ملحوظ. عند التكبير على عملية استدعاء، يتضح أنّ معظم الوقت يتم استغراقه في عمليات التحقّق من الانعكاس.
تنفّذ أنماط "كوروتين" بنية شجرة غير مقفلة للعلاقات بين الأصل والفرع، ما يتيح التزامن المنظّم. تبيّن أنّ مكتبة 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 مرة. ويؤكّد ذلك أنّ Android Runtime (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 .
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!
إلى جانب ذلك، يُجري فريق Android Runtime (ART) عمليات التحسين هذه بشكل أصلي على مستوى الجهاز الظاهري. إذا كان تطبيقك يستهدف مستوى واجهة برمجة التطبيقات 36 ويعمل على إصدار حديث من Android، فمن المحتمل أنّ جهازك يُحسِّن أنماط "كوروتين" بطريقة مماثلة. لاحظت مقاييس أنماط "كوروتين" أعلاه تحسينًا في الأداء بنحو% 15 بعد تحديثات عمليات التجميع في الوقت المناسب في الإصدارات الحديثة من Android Runtime (ART).
سيتلقّى تطبيقك عملية التحسين هذه تلقائيًا عند الترقية إلى الإصدار 9.2.0 من المكوّن الإضافي لنظام Gradle المتوافق مع Android (AGP) أو باستخدام الإصدار 9.2.0 من R8 مباشرةً. لمزيد من المعلومات، يُرجى الاطّلاع على أداة D8 dexer وأداة R8 shrinker.
-
دراسات حالةمن المعروف أنّ حالات التراجع في الأداء يصعب إعادة إنتاجها، ما يجعلها عنق زجاجة كبيرًا لمطوّري الأجهزة الجوّالة.
Alice Yuan, Arti Arutiunov, Nikita Ogorodnikov • قراءة لمدة 4 دقائق -
دراسات حالةشهد تطبيق FotMob مؤخرًا أكبر زيادة ليوم واحد على Wear OS بين جمهوره الذي ثبّت التطبيق خلال 5 سنوات، أي من ضِعف إلى ثلاثة أضعاف المتوسط اليومي. ما السر؟ إنّه مسار بسيط للتثبيت على أجهزة متعددة يساعد المستخدمين في اكتشاف تطبيقهم على Wear OS مباشرةً من هواتفهم.
Garan Jenkin • قراءة لمدة 3 دقائق -
دراسات حالةيشجّع تطبيق Gratitude على الاستمرار من خلال تدوين اليوميات اليومية الصغيرة والعبارات التشجيعية ولوحات الرؤية. تم تنزيل التطبيق أكثر من 6 ملايين مرة، وحصل على 150 ألف تقييم من 5 نجوم، وتم تسجيل 100 مليون إدخال في اليوميات.
Amrit Sanjeev, Ash Nohe • قراءة لمدة 3 دقائق
يمكنك تلقّي أحدث الإحصاءات حول تطوير Android في بريدك الوارد أسبوعيًا.