ตั้งแต่ AGP 9.2.0 เป็นต้นไป R8 จะเพิ่มประสิทธิภาพการเรียกใช้ Atomic*FieldUpdater ส่วนใหญ่ให้เป็นตัวแปร Unsafe ซึ่งทำงานได้ดีขึ้น 2-4 เท่าในการดำเนินการทั่วไป การดำเนินการนี้ส่งผลอย่างมากต่อไลบรารี kotlinx.atomicfu ที่ใช้ Atomic สำหรับ kotlinx.coroutines ซึ่งทำให้การเปิดใช้และการยกเลิก Coroutines เร็วขึ้นสูงสุด 2 เท่า หากต้องการรับสิทธิประโยชน์ ให้อัปเดต AGP เป็น 9.2.0 ขึ้นไป
เนื่องจากแอป Android ส่วนใหญ่เลือกใช้ Kotlin เป็นภาษาหลัก kotlinx.coroutines จึงกลายเป็นมาตรฐานโดยพฤตินัยสำหรับการเขียนโปรแกรมแบบอะซิงโครนัส ไลบรารีนี้มีวิธีจัดการโฟลว์พร้อมกันที่ออกแบบมาอย่างดีและมีโครงสร้าง ซึ่งเป็นฟีเจอร์ที่มีอยู่ใน Kotlin Jetpack Compose ก็เช่นกัน โดยใช้ Coroutines เพื่อจัดการเหตุการณ์ตัวชี้ ภาพเคลื่อนไหว และการโต้ตอบอื่นๆ ในขณะที่เขียนบทความนี้ API พร้อมกันส่วนใหญ่ใน Compose จะเรียกใช้ฟังก์ชัน suspend เบื้องหลัง และเปิดใช้และ/หรือยกเลิก Coroutines เพื่อจัดการการอัปเดต
เมื่อทีม Compose เริ่มตรวจสอบประสิทธิภาพ ก็พบว่า โครูทีน เป็น จุดคอขวด สำหรับการดำเนินการจำนวนมากที่เกิดขึ้นนอก Composition ตัวอย่างเช่น 80% ของเวลาที่ใช้ในการสร้างและอัปเดต Modifier.clickable ถูกใช้ไปกับการเปิดใช้และยกเลิก Coroutines ภายในที่จัดการการอัปเดต InteractionSource จากข้อสังเกตดังกล่าว งานด้านประสิทธิภาพในช่วงแรกๆ จึงมุ่งเน้นไปที่การนำ Coroutines ออกจากเส้นทางเริ่มต้นและชะลอการเริ่มต้นจนกว่าจะจำเป็น
ค่าใช้จ่ายของ Coroutine
วิธีที่ง่ายที่สุดในการวิเคราะห์ลักษณะการทำงานภายในของฟังก์ชันใน Android คือการบันทึกร่องรอยเมธอด Android Runtime (ART) ร่องรอยเมธอด ART เป็นเครื่องมือที่บันทึกโฟลว์การดำเนินการของแอป โดยแสดงเมธอดที่เรียกใช้ ลำดับ และเวลาที่ใช้ในแต่ละเมธอดอย่างแม่นยำ ซึ่งช่วยให้นักพัฒนาแอปสามารถระบุจุดคอขวดด้านประสิทธิภาพได้ สำหรับการเรียกใช้ LaunchedEffect { } ที่ว่างเปล่า การติดตามเมธอดจะมีลักษณะดังนี้
ร่องรอยเมธอดด้านบนสามารถแบ่งออกเป็น 3 ส่วน ได้แก่
- การเริ่มต้น Coroutine ใหม่
- การเริ่ม Coroutine
- การสิ้นสุด Coroutine (เนื่องจากออกจากระบบทันที)
การยกเลิก LaunchedEffect คล้ายกับการสิ้นสุดตามปกติ ยกเว้นว่าจะสร้าง CancellationException ด้วย
จากโปรไฟล์ด้านบน สิ่งหนึ่งที่น่าสงสัยทันทีคือการเรียกใช้ java.util.concurrent.AtomicReferenceFieldUpdater บ่อยครั้ง (กล่องสีม่วงหรือสีเขียวที่มีป้ายกำกับ j…) แม้ว่าการเรียกใช้แต่ละครั้งจะค่อนข้างเร็ว แต่ความถี่ก็เป็นเรื่องที่น่ากังวล ค่าใช้จ่ายที่ไม่เล็กน้อยซึ่งกระจายอยู่ในการเรียกใช้หลายครั้งอาจรวมกันเป็นค่าใช้จ่ายที่เพิ่มขึ้นอย่างเห็นได้ชัด เมื่อซูมดูการเรียกใช้ จะพบว่าเวลาส่วนใหญ่ใช้ไปกับการตรวจสอบการสะท้อน
Coroutines ใช้โครงสร้างต้นไม้แบบไม่มีการล็อกสำหรับความสัมพันธ์หลัก-ย่อย ซึ่งทำให้เกิดการทำงานพร้อมกันแบบมีโครงสร้าง ปรากฏว่าไลบรารี kotlinx.atomicfu ใช้การดำเนินการ Atomic แบบไม่มีการล็อกโดยใช้ Primitive JVM ที่รู้จักกันดีอย่าง AtomicReferenceFieldUpdater Updater ใช้การอ้างอิงคลาสและชื่อฟิลด์เพื่อดำเนินการ Atomic ในรันไทม์ และต้องเรียกใช้การตรวจสอบความปลอดภัยแบบสะท้อนหลายครั้งเพื่อให้แน่ใจว่าฟิลด์มีอยู่และเข้าถึงได้ การดำเนินการแต่ละอย่างใน Coroutines (การเริ่มต้น การระงับ การยกเลิก การสิ้นสุด) จะเรียกใช้การดำเนินการ Atomic อย่างน้อย 1 รายการ ดังนั้นหากการดำเนินการ Atomic ช้า Coroutines ก็จะทำงานได้ไม่ดี
การตรวจสอบ AtomicReferenceFieldUpdater
แต่เราจะยังไม่พูดถึงเรื่องนี้AtomicReferenceFieldUpdater ได้รับการเพิ่มประสิทธิภาพอย่างดีใน JVM มานานกว่า 10 ปีแล้ว และการติดตามเมธอดอาจบันทึกค่าใช้จ่ายที่การเพิ่มประสิทธิภาพระดับ VM เช่น การคอมไพล์แบบ Just-In-Time (JIT) หรือ Ahead-Of-Time (AOT) นำออกไปโดยสมบูรณ์ หากต้องการยืนยันประสิทธิภาพ ให้เขียนการทดสอบประสิทธิภาพ 2-3 รายการเพื่อวัดความแตกต่างระหว่างการอ้างอิง Atomic จาก 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 ภายใน ซึ่งจะดำเนินการ Atomic พื้นฐาน ในกรณีส่วนใหญ่ ตัวเริ่มต้น Updater จะเป็นแบบคงที่ และสามารถพิสูจน์ได้ว่าถูกต้องเสมอตามโครงสร้างของคลาสโดยรอบ ดังนั้น จึงสามารถวิเคราะห์การใช้งาน AtomicReferenceFieldUpdater ส่วนใหญ่แบบคงที่และแทนที่ด้วยตัวแปร Unsafe ภายในระหว่างการคอมไพล์ นอกจากนี้ Toolchain การสร้าง 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"); // ... } }
ตัวอย่างพื้นฐานจะสร้าง Updater แบบคงที่ขั้นสุดท้ายซึ่งเข้าถึงฟิลด์ Volatile ด้วยอาร์กิวเมนต์ค่าคงที่แบบง่ายสำหรับ Holder, ประเภท และชื่อฟิลด์ การสะท้อนที่ใช้นั้นโปร่งใสโดยสมบูรณ์ เห็นได้ชัดว่า Updater นี้อ้างอิงฟิลด์ที่ถูกต้อง และไซต์การสร้าง Updater มีสิทธิ์เข้าถึงฟิลด์ที่ถูกต้อง
โดยพื้นฐานแล้ว Atomic*FieldUpdater เป็น Wrapper รอบๆ ออฟเซ็ตฟิลด์และการเรียกใช้ Unsafe สถานการณ์ที่ดีที่สุดสำหรับการเพิ่มประสิทธิภาพคือการแทนที่ฟิลด์ Updater ด้วยฟิลด์ออฟเซ็ต และแทนที่การเรียกใช้ Updater ด้วยการเรียกใช้ Unsafe
การเพิ่มประสิทธิภาพ Atomic*FieldUpdater
การเพิ่มประสิทธิภาพจะดำเนินการใน 3 ส่วน ได้แก่ การใช้เครื่องมือ การแทนที่ และการจัดระเบียบ
การใช้เครื่องมือ
ขั้นตอนแรกคือการนำฟิลด์ออฟเซ็ตมาใช้ควบคู่กับฟิลด์ Updater เพื่อให้เข้าถึงได้โดยตรงผ่านการเรียกใช้ Unsafe
static final long updater$offset = SyntheticUnsafe.UNSAFE.objectFieldOffset(Example.class.getDeclaredField("data"))
ระบบจะเข้าถึงฟิลด์ผ่านการสะท้อน และใช้ Unsafe เพื่อดึงออฟเซ็ตฟิลด์ในคลาส โค้ดนี้แสดงถึงส่วนประกอบภายในของ Atomic*FieldUpdater หากคุณไม่สนใจการตรวจสอบการสะท้อน แต่ระบบจะติดตามประเภท Holder ของ Updater และประเภทฟิลด์ของฟิลด์ Volatile แบบคงที่ในคอมไพเลอร์
โปรดทราบว่าระบบจะปล่อยให้ฟิลด์เดิมและการเริ่มต้นของฟิลด์เป็นไปตามเดิม กระบวนการเพิ่มประสิทธิภาพจะอำนวยความสะดวกและเพิ่มประสิทธิภาพการใช้งานอย่างเต็มที่ แล้วจึงจัดระเบียบในภายหลัง นี่เป็นแนวทางที่ง่ายในการใช้งาน แต่ยังช่วยให้เพิ่มประสิทธิภาพฟิลด์ Updater ได้บางส่วน โดยที่การใช้งานบางอย่างยังคงเป็นไปตามเดิมในขณะที่การใช้งานอื่นๆ ได้รับการเพิ่มประสิทธิภาพ
การแทนที่
ในจุดนี้ในคอมไพเลอร์ หลังจากจุดรวมการทำงานพร้อมกันที่เหมาะสมแล้ว เราจะมีรายการฟิลด์ Updater ที่ใช้เครื่องมือ ซึ่งหมายความว่าเราสามารถเพิ่มประสิทธิภาพแต่ละไซต์การเรียกใช้ได้ทีละรายการตามเงื่อนไข 2-3 ข้อ ลองดูตัวอย่างการเรียกใช้ต่อไปนี้
updater.compareAndSet(holder, expectedValue, newValue);
เงื่อนไขที่ Atomic*FieldUpdater กำหนดมีดังนี้
updaterมาจากฟิลด์ที่ใช้เครื่องมือหรือไม่ กล่าวคือ การวิเคราะห์แบบคงที่สามารถติดตามค่าของออบเจ็กต์กลับไปเป็นการอ่านฟิลด์ของ Updater ที่ใช้เครื่องมือได้หรือไม่holderเป็นคลาสเดียวกันหรือคลาสย่อยของประเภท Holder ที่กำหนดไว้เดิมหรือไม่newValueเป็นคลาสเดียวกันหรือคลาสย่อยของประเภทฟิลด์ที่กำหนดไว้เดิมหรือไม่
หากตรงตามเงื่อนไขทั้งหมด ระบบจะแทนที่การเรียกใช้ด้วยการเรียกใช้ Unsafe โดยไม่มีการตรวจสอบการสะท้อน
SyntheticUnsafe.UNSAFE.compareAndSwapObject(holder, Example.updater$offset, expectedValue, newValue)
การเรียกใช้ใหม่นี้เร็วกว่าและง่ายกว่า แต่แตกต่างจากการเรียกใช้เดิมในเรื่องการจัดการค่า Null ใน updater และ holder ระบบจะแทรกการตรวจสอบ Null สำหรับทั้ง 2 รายการ เว้นแต่จะมีการตัดออกแบบคงที่
การจัดระเบียบ
ในจุดนี้ คลาส Holding จะมีฟิลด์ Updater เดิมและฟิลด์ออฟเซ็ตใหม่ รวมถึงไซต์การเรียกใช้ที่อาจใช้ฟิลด์ใดฟิลด์หนึ่ง หากไม่มีการเพิ่มประสิทธิภาพไซต์การเรียกใช้ ระบบควรนำฟิลด์ออฟเซ็ตออก และหากมีการเพิ่มประสิทธิภาพไซต์การเรียกใช้ทั้งหมด ระบบควรนำฟิลด์ Updater ออก ในทั้ง 2 กรณี ระบบควรลบการเรียกใช้การเริ่มต้นด้วย คอมไพเลอร์จะลบฟิลด์ที่ไม่ได้ใช้และการนำโค้ดที่ไม่ได้ใช้แล้วออก แต่การนำโค้ดการเริ่มต้นออกในที่นี้ต้องใช้เทคนิคเพิ่มเติม
ทั้งการเรียกใช้ newUpdater และ getDeclaredField อาจมีผลข้างเคียงเนื่องจากอาจส่งข้อยกเว้น (และเราไม่ทราบการใช้งานเนื่องจากขึ้นอยู่กับเวอร์ชัน API) ซึ่งหมายความว่าการเพิ่มประสิทธิภาพทั่วไปไม่สามารถนำออกได้อย่างปลอดภัย ดังนั้น การจัดระเบียบนี้จึงต้องพิจารณาฟิลด์ที่ใช้เครื่องมืออย่างชัดเจน เนื่องจากทราบแบบคงที่ว่าฟิลด์เหล่านั้นไม่มีข้อยกเว้น
ในท้ายที่สุด ตัวอย่าง Updater แบบง่ายที่แสดงด้านบนจะมีลักษณะดังนี้หลังการเพิ่มประสิทธิภาพ
ผลลัพธ์
หลังจากการเพิ่มประสิทธิภาพเหล่านี้ kotlinx.atomicfu และการใช้งาน AtomicInt/Long/ReferenceFieldUpdater อย่างชัดเจนส่วนใหญ่จะตรงกับประสิทธิภาพของ AtomicReference เมื่อใช้ R8 ซึ่งในความเป็นจริงแล้วยังเร็วกว่าในการทดสอบประสิทธิภาพบางรายการ kotlinx.atomicfu มี ปลั๊กอินคอมไพเลอร์ ที่สามารถอินไลน์อินสแตนซ์ atomic ลงในฟิลด์ ซึ่งช่วยลดการจัดสรรที่จำเป็นในการสร้างฟิลด์ที่อัปเดตแบบ Atomic
Jetpack Compose เป็นผู้ได้รับประโยชน์หลักจากงานนี้ รันไทม์ของ Compose มีการทดสอบประสิทธิภาพขนาดเล็กจำนวนมากที่ติดตามประสิทธิภาพของ Coroutine อย่างใกล้ชิดเพื่อตรวจหาการลดลงของประสิทธิภาพตั้งแต่เนิ่นๆ เมื่ออัปเดตการทดสอบประสิทธิภาพเป็น R8 เวอร์ชันใหม่ เราพบว่าประสิทธิภาพดีขึ้น 2 เท่า เมื่อเปิดใช้และยกเลิก Coroutines ในLaunchedEffect!
นอกจากนี้ ทีม ART ยังใช้การเพิ่มประสิทธิภาพเหล่านี้แบบเนทีฟในระดับ VM หากแอปกำหนดเป้าหมายเป็น API 36 และทำงานใน Android เวอร์ชันล่าสุด อุปกรณ์ของคุณอาจเพิ่มประสิทธิภาพ Coroutines ในลักษณะที่คล้ายกันอยู่แล้ว การทดสอบประสิทธิภาพ Coroutine ด้านบนพบว่าประสิทธิภาพดีขึ้น ~15% หลังจากการอัปเดต JIT ใน ART เวอร์ชันล่าสุด
แอปจะได้รับการเพิ่มประสิทธิภาพนี้โดยค่าเริ่มต้นเมื่ออัปเกรดเป็น AGP 9.2.0 หรือใช้ R8 9.2.0 โดยตรง โปรดดูข้อมูลเพิ่มเติมที่ D8 Dexer และ R8 Shrinker
-
กรณีศึกษาการลดลงของประสิทธิภาพเป็นเรื่องยากที่จะสร้างข้อผิดพลาดซ้ำ ซึ่งทำให้การลดลงของประสิทธิภาพเป็นจุดคอขวดขนาดใหญ่สำหรับนักพัฒนาแอปบนอุปกรณ์เคลื่อนที่
Alice Yuan, Arti Arutiunov, Nikita Ogorodnikov • ใช้เวลาอ่าน 4 นาที -
กรณีศึกษาเมื่อเร็วๆ นี้ FotMob มีการเพิ่มขึ้นมากที่สุดในวันเดียวใน Wear OS ในกลุ่มผู้ชมที่ติดตั้งแอปในช่วง 5 ปีที่ผ่านมา โดยเพิ่มขึ้น 2-3 เท่าของค่าเฉลี่ยรายวัน เคล็ดลับคืออะไร โฟลว์การติดตั้งข้ามอุปกรณ์แบบง่ายที่ช่วยให้ผู้ใช้ค้นพบแอป Wear OS ได้โดยตรงจากโทรศัพท์
Garan Jenkin • ใช้เวลาอ่าน 3 นาที -
กรณีศึกษาแอป Gratitude ซึ่งเป็นแอปฝึกสติช่วยส่งเสริมความสม่ำเสมอผ่านการจดบันทึกประจำวันขนาดเล็ก ข้อความเสริมสร้างกำลังใจ และวิชันบอร์ด แอปนี้มีการดาวน์โหลดมากกว่า 6 ล้านครั้ง การให้คะแนน 5 ดาว 150,000 ครั้ง และมีการบันทึกรายการบันทึก 100 ล้านรายการ
Amrit Sanjeev, Ash Nohe • ใช้เวลาอ่าน 3 นาที
รับข้อมูลเชิงลึกด้านการพัฒนาแอป Android ล่าสุดส่งตรงถึงกล่องจดหมายของคุณ ทุกสัปดาห์