ตั้งแต่ 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 เริ่มตรวจสอบประสิทธิภาพ ก็พบว่า Coroutines เป็นคอขวดสำหรับการดำเนินการจำนวนมากที่เกิดขึ้นนอกการคอมโพส ตัวอย่างเช่น 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 รายการ เว้นแต่จะมีการตัดออกแบบคงที่
การจัดระเบียบ
ในจุดนี้ คลาส Holder จะมีฟิลด์ 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
-
กรณีศึกษาWhatsApp เป็นแพลตฟอร์มการรับส่งข้อความที่ใหญ่ที่สุดในโลก โดยให้บริการผู้ใช้หลายพันล้านคนทั่วโลก เครื่องมือนี้เป็นเครื่องมือสื่อสารเริ่มต้นสำหรับผู้คนในภูมิภาคต่างๆ โดยเชื่อมต่อผู้ใช้ผ่านการรับส่งข้อความส่วนตัวที่เชื่อถือได้และปลอดภัย
Niharika Arora, Tracy Agyemang, Mayank Jain • ใช้เวลาอ่าน 8 นาที -
กรณีศึกษาTinder มีภารกิจในการส่งเสริมและสร้างแรงบันดาลใจให้เกิดความสัมพันธ์ที่แท้จริงด้วยการทำให้การพบปะเป็นเรื่องง่ายและสนุกสำหรับคนโสดรุ่นใหม่ทุกรุ่น
Ajesh Pai, Ulises Uriel Verduzco Díaz , Tracy Agyemang • ใช้เวลาอ่าน 4 นาที -
กรณีศึกษาการถดถอยด้านประสิทธิภาพเป็นเรื่องยากที่จะสร้างข้อผิดพลาดซ้ำ ซึ่งทำให้การถดถอยเป็นจุดคอขวดขนาดใหญ่สำหรับนักพัฒนาแอปบนอุปกรณ์เคลื่อนที่
Alice Yuan, Arti Arutiunov, Nikita Ogorodnikov • ใช้เวลาอ่าน 4 นาที
รับข้อมูลเชิงลึกด้านการพัฒนาแอป Android ล่าสุดส่งตรงถึงกล่องจดหมายของคุณ ทุกสัปดาห์