نشرها أندريه شيكوف، مهندس برمجيات أول، مجموعة أدوات Android وجوناثان ستاروب، مهندس برمجيات، فريق R8

بدءًا من AGP 9.2.0، يعمل R8 على تحسين الأداء بشكل أكبر Atomic*FieldUpdater يدعو إلى Unsafe المتغيرات التي تقدم أداءً أفضل بمقدار 2x إلى 4x في العمليات المشتركة. وهذا له تأثير كبير بشكل خاص على مكتبة kotlinx.atomicfu التي تنفذ علم الذرات kotlinx.coroutines، مما يجعل إطلاق وإلغاء coroutines أسرع بما يصل إلى 2x. للحصول على المزايا، قم بتحديث AGP الخاص بك إلى الإصدار 9.2.0 أو أعلى.
مع اعتماد غالبية تطبيقات Android على لغة Kotlin كلغة رئيسية مفضلة، kotlinx.coroutines لقد أصبح المعيار الفعلي للبرمجة غير المتزامنة. تقدم المكتبة طريقة جيدة التصميم ومنظمة لإدارة التدفقات المتزامنة الأصلية في Kotlin. لم يكن Jetpack Compose استثناءً، حيث اعتمد إجراءات روتينية لإدارة أحداث المؤشر والرسوم المتحركة والتفاعلات الأخرى. في وقت كتابة هذا التقرير، كانت معظم واجهات برمجة التطبيقات المتزامنة قيد الاتصال suspend تعمل تحت الغطاء وتقوم بتشغيل و/أو إلغاء coroutines للتعامل مع التحديثات.
عندما بدأ فريق Compose في التحقيق في الأداء، تم اكتشاف أن الكوروتينات تمثل عنق الزجاجة للعديد من العمليات التي تحدث خارج التكوين. على سبيل المثال، تم قضاء 80% من الوقت في الإنشاء والتحديث Modifier.clickable تم استهلاكه من خلال إطلاق وإلغاء coroutines الداخلية التي تم التعامل معها InteractionSource التحديثات. بناءً على تلك الملاحظات، ركز الكثير من أعمال الأداء المبكرة على إزالة الكوروتينات من المسار الافتراضي وتأخير التهيئة حتى الضرورة.
تكلفة الكوروتين
أسهل طريقة لتحليل السلوك الداخلي لوظيفة ما على Android هي التقاط تتبع أسلوب Android Runtime (ART). تتبع أسلوب ART هو أداة تسجل تدفق تنفيذ التطبيق، وتظهر بالضبط الأساليب التي يتم استدعاؤها وترتيبها ومقدار الوقت المستغرق في كل منها، مما يسمح للمطورين بتحديد اختناقات الأداء. لفارغة LaunchedEffect { } المكالمة، سيبدو الأمر كالتالي:

يتم عرض تتبع طريقة LaunchedEffect في واجهة مستخدم Perfetto
يمكن تقسيم تتبع الطريقة أعلاه إلى ثلاثة أجزاء:
-
تهيئة كوروتين جديد
-
البدء بالكوروتين
-
إكمال الكوروتين (لأنه يخرج على الفور)
الإلغاء LaunchedEffect يشبه الإكمال العادي، إلا أنه ينشئ أيضًا ملفًا CancellationException.
من الملف الشخصي أعلاه، هناك شيء واحد مريب على الفور وهو المكالمات المتكررة java.util.concurrent.AtomicReferenceFieldUpdater (مربعات أرجوانية أو خضراء تحمل ملصقات j). في حين أن كل مكالمة سريعة نسبيًا، إلا أن تكرارها مثير للقلق؛ أي حمل غير مهم يتم توزيعه عبر استدعاءات متعددة قد يؤدي إلى تراجع ملحوظ. تكبير المكالمة يكشف أن معظم الوقت يتم إنفاقه على… التحقق من الانعكاس؟

نظرة عن قرب على تتبع أسلوب AtomicReferenceFieldUpdater.get أثناء تهيئة LaunchedEffect
تطبق Coroutines بنية شجرة خالية من القفل للعلاقات بين الوالدين والطفل مما يجعل التزامن المنظم ممكنًا. تبين أن kotlinx.atomicfu تنفذ المكتبة عمليات ذرية خالية من القفل باستخدام لغة JVM البدائية المعروفة، AtomicReferenceFieldUpdater. يستخدم المُحدِّث مرجع فئة واسم حقل لإجراء عمليات ذرية في وقت التشغيل، وعليه إجراء العديد من اختبارات السلامة العاكسة للتأكد من وجود الحقل وإمكانية الوصول إليه. كل عملية في الكوروتينات (البدء، التعليق، الإلغاء، الإكمال) تستدعي عملية ذرية واحدة على الأقل، لذلك إذا كانت بطيئة، فلن تعمل الكوروتينات بشكل جيد.
التحقيق في AtomicReferenceFieldUpdater
لكن دعونا لا نتقدم على أنفسنا. AtomicReferenceFieldUpdater تم تحسينه بشكل جيد بالفعل على JVM لأكثر من 10 سنوات حتى الآن، وقد تلتقط تتبعات الطريقة الحمل الزائد الذي تمت إزالته بالكامل عن طريق تحسين مستوى VM: التجميعات في الوقت المناسب (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 يتم تجميع JIT أثناء عملية الإحماء)، مما يؤدي إلى النتائج التالية على Pixel 5 (API 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.
تحسين الذرية * 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 قد يكون لها آثار جانبية لأنها يمكن أن تطرح استثناءات (كما أن تنفيذها غير معروف أيضًا لأنه يعتمد على إصدار واجهة برمجة التطبيقات). وهذا يعني أنه من خلال التحسين العام، لا يمكن إزالتها بأمان. لذلك، تطلبت عملية التنظيف هذه دراسة واضحة للمجالات المُجهزة، نظرًا لأنها معروفة بشكل ثابت بأنها خالية من الاستثناءات.
في النهاية، يبدو مثال التحديث البسيط الموضح أعلاه كما يلي بعد التحسين:
class Example {
volatile String data = "";
static final long updater$offset =
SyntheticUnsafe.UNSAFE.objectFieldOffset(Example.class.getDeclaredField("data"))
void example() {
// ...
SyntheticUnsafe.UNSAFE.compareAndSwapObject(this, Example.updater$offset, "", "new")
// ...
}
}
نتائج
بعد هذه التحسينات، kotlinx.atomicfu والاستخدامات الأكثر وضوحا ل AtomicInt/Long/ReferenceFieldUpdater تطابق الآن AtomicReference الأداء مع تطبيق R8. وفي الواقع، فهو أسرع في بعض المعايير؛ kotlinx.atomicfu يحتوي على مكون إضافي للمترجم يمكنه تضمينه atomic مثيلات في الحقول، مما يقلل من التخصيصات المطلوبة لإنشاء حقل محدث ذريًا.
كان Jetpack Compose هو المستفيد الرئيسي من هذا العمل. يحتوي وقت تشغيل الإنشاء على عدد من المعايير الدقيقة التي تتتبع أداء الكوروتين عن كثب لاكتشاف تراجعات الأداء مبكرًا. عندما تم تحديث المعايير إلى إصدار جديد من R8، لاحظنا تحسنًا بمقدار 2x عند إطلاق وإلغاء coroutines في LaunchedEffect!

رسم بياني مرجعي يوضح الوقت المستغرق عند بدء وإلغاء coroutines في LaunchedEffect (الأقل هو الأفضل). يتوافق التغيير في الرسم البياني مع تحديث R8، الذي يعرض تحسنًا بمقدار 2x.
وبصرف النظر عن ذلك، يقوم فريق ART بتنفيذ هذه التحسينات محليًا على مستوى الأجهزة الافتراضية. إذا كان تطبيقك يستهدف API 36 ويعمل على إصدار حديث من Android، فمن المحتمل أن جهازك يعمل بالفعل على تحسين coroutines بطريقة مماثلة. لاحظت معايير coroutine المذكورة أعلاه تحسنًا بنسبة 15% تقريبًا في الأداء بعد تحديثات JIT في الإصدارات الأخيرة من ART.
سيتلقى تطبيقك هذا التحسين افتراضيًا عند الترقية إلى AGP 9.2.0 أو باستخدام R8 9.2.0 مباشرة. لمزيد من المعلومات، راجع D8 dexer وR8 تقليص.

