प्रॉडक्ट से जुड़ी खबरें

18% तेज़ी से कंपाइल करने की सुविधा, परफ़ॉर्मेंस से कोई समझौता नहीं

आठ मिनट में पढ़ें
Santiago Aboy Solanes और Vladimír Marko

Android Runtime (ART) की टीम ने कंपाइल करने में लगने वाले समय को 18% तक कम कर दिया है. हालांकि, कंपाइल किए गए कोड या मेमोरी से जुड़ी किसी भी समस्या से कोई समझौता नहीं किया गया है. यह सुधार, कंपाइल करने में लगने वाले समय को बेहतर बनाने की हमारी 2025 की पहल का हिस्सा था. इसके लिए, मेमोरी के इस्तेमाल या कंपाइल किए गए कोड की क्वालिटी से कोई समझौता नहीं किया गया.

ART के लिए, कंपाइल करने में लगने वाले समय की स्पीड को ऑप्टिमाइज़ करना ज़रूरी है. उदाहरण के लिए, जस्ट-इन-टाइम (JIT) कंपाइल करने पर, यह सीधे तौर पर ऐप्लिकेशन की परफ़ॉर्मेंस और डिवाइस की कुल परफ़ॉर्मेंस पर असर डालता है. तेज़ी से कंपाइल करने पर, ऑप्टिमाइज़ेशन शुरू होने में लगने वाला समय कम हो जाता है. इससे, लोगों को बेहतर और ज़्यादा रिस्पॉन्सिव अनुभव मिलता है. इसके अलावा, JIT और अहेड-ऑफ़-टाइम (AOT) दोनों के लिए, कंपाइल करने में लगने वाले समय की स्पीड में सुधार करने से, कंपाइल करने की प्रोसेस के दौरान संसाधनों का इस्तेमाल कम होता है. इससे बैटरी लाइफ़ और डिवाइस के तापमान को बेहतर बनाने में मदद मिलती है. खास तौर पर, कम सुविधाओं वाले डिवाइसों पर.

कंपाइल करने में लगने वाले समय की स्पीड में किए गए कुछ सुधार, जून 2025 में Android के वर्शन में लॉन्च किए गए थे. बाकी सुधार, साल के आखिर में Android के वर्शन में उपलब्ध होंगे. इसके अलावा, Android के वर्शन 12 और उसके बाद के वर्शन इस्तेमाल करने वाले सभी लोगों को, मेनलाइन अपडेट के ज़रिए ये सुधार मिल सकते हैं.

ऑप्टिमाइज़ करने वाले कंपाइलर को ऑप्टिमाइज़ करना

किसी कंपाइलर को ऑप्टिमाइज़ करना हमेशा एक मुश्किल काम होता है. आपको स्पीड मुफ़्त में नहीं मिल सकती. इसके लिए, आपको कुछ छोड़ना होगा. हमने अपने लिए एक बहुत साफ़ और मुश्किल लक्ष्य तय किया है: कंपाइलर को तेज़ बनाना, लेकिन ऐसा मेमोरी से जुड़ी समस्याओं को ठीक करने और सबसे ज़रूरी बात यह है कि कंपाइलर से जनरेट होने वाले कोड की क्वालिटी को कम किए बिना करना. अगर कंपाइलर तेज़ है, लेकिन ऐप्लिकेशन धीरे चलते हैं, तो हम फ़ेल हो गए हैं.

हमने इन ज़रूरी शर्तों को पूरा करने के लिए, अपने डेवलपमेंट के समय को खर्च करने का फ़ैसला लिया. इसके लिए, हमने गहराई से जांच की और ऐसे बेहतर समाधान खोजे जो इन ज़रूरी शर्तों को पूरा करते हों. आइए, इस बारे में ज़्यादा जानें कि हम सुधार के लिए जगहें ढूंढने के साथ-साथ, अलग-अलग समस्याओं के सही समाधान कैसे ढूंढते हैं.

ऑप्टिमाइज़ेशन के लिए काम के विकल्प ढूंढना

किसी मेट्रिक को ऑप्टिमाइज़ करने से पहले, आपको उसे मेज़र करना होगा. ऐसा न करने पर, आपको कभी भी यह पक्का नहीं हो पाएगा कि आपने उसे बेहतर बनाया है या नहीं. हमारे लिए अच्छी बात यह है कि कंपाइल करने में लगने वाले समय की स्पीड में काफ़ी समानता होती है. हालांकि, इसके लिए आपको कुछ सावधानियां बरतनी होंगी. जैसे, बदलाव से पहले और बाद में मेज़र करने के लिए एक ही डिवाइस का इस्तेमाल करना और यह पक्का करना कि आपका डिवाइस ज़्यादा गर्म न हो. इसके अलावा, हमारे पास कंपाइलर के आंकड़ों जैसे तय मेज़रमेंट भी होते हैं. इनसे हमें यह समझने में मदद मिलती है कि बैकग्राउंड में क्या हो रहा है.

 

इन सुधारों के लिए, हम अपने डेवलपमेंट के समय को खर्च कर रहे थे. इसलिए, हम जितनी तेज़ी से हो सके, उतनी तेज़ी से बदलाव करना चाहते थे. इसका मतलब है कि हमने समाधानों का प्रोटोटाइप बनाने के लिए, कुछ प्रतिनिधि ऐप्लिकेशन (पहले पक्ष के ऐप्लिकेशन, तीसरे पक्ष के ऐप्लिकेशन, और Android ऑपरेटिंग सिस्टम का मिक्स) लिए. बाद में, हमने मैन्युअल और ऑटोमेटेड, दोनों तरह की टेस्टिंग करके यह पुष्टि की कि फ़ाइनल वर्शन काम का है.

 

हमने चुने गए एपीके के सेट के साथ, स्थानीय तौर पर मैन्युअल कंपाइल करने की प्रोसेस शुरू की. इसके बाद, कंपाइल करने की प्रोफ़ाइल ली और यह देखने के लिए pprof का इस्तेमाल किया कि हम अपना समय कहां खर्च कर रहे हैं.

image.png

pprof में, किसी प्रोफ़ाइल के फ़्लेम ग्राफ़ का उदाहरण

pprof टूल बहुत काम का है. इसकी मदद से, हम डेटा को स्लाइस, फ़िल्टर, और सॉर्ट कर सकते हैं. उदाहरण के लिए, यह देखा जा सकता है कि कंपाइलर के किन फ़ेज़ या तरीकों में ज़्यादा समय लग रहा है. हम pprof के बारे में ज़्यादा जानकारी नहीं देंगे. बस यह जान लें कि अगर बार बड़ा है, तो इसका मतलब है कि कंपाइल करने में ज़्यादा समय लगा.

इनमें से एक व्यू “बॉटम अप” है. इसमें यह देखा जा सकता है कि किन तरीकों में ज़्यादा समय लग रहा है. नीचे दी गई इमेज में, हम Kill नाम का एक तरीका देख सकते हैं. इसमें कंपाइल करने में लगने वाले समय का 1% से ज़्यादा समय लग रहा है. ब्लॉग पोस्ट में, टॉप पर रहने वाले कुछ अन्य तरीकों के बारे में भी बाद में बताया जाएगा.

image.png

किसी प्रोफ़ाइल का बॉटम अप व्यू

हमारे ऑप्टिमाइज़ करने वाले कंपाइलर में, ग्लोबल वैल्यू नंबरिंग (GVN) नाम का एक फ़ेज़ होता है. आपको इस बारे में चिंता करने की ज़रूरत नहीं है कि यह पूरी तरह से क्या करता है. हालांकि, यह जानना ज़रूरी है कि इसमें `Kill` नाम का एक तरीका है. इसकी मदद से, फ़िल्टर के हिसाब से कुछ नोड मिटाए जा सकते हैं. इसमें ज़्यादा समय लगता है, क्योंकि इसे सभी नोड को बारी-बारी से देखना और जांचना होता है. हमने पाया कि कुछ मामलों में हमें पहले से पता होता है कि जांच गलत होगी. भले ही, उस समय हमारे पास कोई भी नोड मौजूद हो. इन मामलों में, हम पूरी तरह से जांच को छोड़ सकते हैं. इससे, जांच में लगने वाला समय 1.023% से घटकर ~0.3% हो जाता है. साथ ही, GVN के रनटाइम में ~15% का सुधार होता है.

ऑप्टिमाइज़ेशन के लिए काम के विकल्प लागू करना

हमने यह बताया कि समय को कैसे मेज़र किया जाए और यह कैसे पता लगाया जाए कि समय कहां खर्च हो रहा है. हालांकि, यह सिर्फ़ शुरुआत है. अगला चरण यह है कि कंपाइल करने में लगने वाले समय को कैसे ऑप्टिमाइज़ किया जाए.

आम तौर पर, ऊपर दिए गए `Kill` जैसे मामले में, हम यह देखते हैं कि नोड को कैसे बारी-बारी से देखा जाए. साथ ही, हम इसे तेज़ी से करने की कोशिश करते हैं. उदाहरण के लिए, समानांतर में काम करके या एल्गोरिदम को बेहतर बनाकर. असल में, हमने सबसे पहले यही कोशिश की. जब हमें कुछ नहीं मिला, तो हमने “एक मिनट रुको…” कहा और देखा कि समाधान यह था कि (कुछ मामलों में) बारी-बारी से देखने की प्रोसेस को पूरी तरह से छोड़ दिया जाए! इस तरह के ऑप्टिमाइज़ेशन करते समय, यह आसानी से हो सकता है कि हम छोटी-छोटी समस्याओं को ठीक करने में इतने व्यस्त हो जाएं कि बड़ी समस्या को अनदेखा कर दें.

अन्य मामलों में, हमने कई अलग-अलग तकनीकों का इस्तेमाल किया. इनमें ये शामिल हैं:

  • यह तय करने के लिए ह्यूरिस्टिक्स का इस्तेमाल करना कि किसी ऑप्टिमाइज़ेशन से काम के नतीजे मिलेंगे या नहीं. अगर नहीं मिलेंगे, तो उसे छोड़ा जा सकता है
  • कंपाइल किए गए डेटा को कैश करने के लिए, अतिरिक्त डेटा स्ट्रक्चर का इस्तेमाल करना
  • स्पीड बढ़ाने के लिए, मौजूदा डेटा स्ट्रक्चर में बदलाव करना
  • कुछ मामलों में साइकल से बचने के लिए, नतीजों को धीरे-धीरे कंपाइल करना
  • सही ऐब्स्ट्रैक्शन का इस्तेमाल करना - गैर-ज़रूरी सुविधाओं से कोड की स्पीड कम हो सकती है
  • बार-बार इस्तेमाल होने वाले पॉइंटर को कई लोड के ज़रिए ऐक्सेस करने से बचना

हमें कैसे पता चलेगा कि ऑप्टिमाइज़ेशन के लिए काम के विकल्प लागू करना सही है या नहीं?

यह एक मुश्किल सवाल है, जिसका जवाब देना आसान नहीं है. यह पता लगाने के बाद कि किसी हिस्से में कंपाइल करने में ज़्यादा समय लग रहा है और उसे बेहतर बनाने के लिए डेवलपमेंट का समय खर्च करने के बाद, कभी-कभी आपको कोई समाधान नहीं मिल पाता. ऐसा हो सकता है कि कुछ न किया जा सके, समाधान लागू करने में बहुत ज़्यादा समय लगे, किसी अन्य मेट्रिक में काफ़ी गिरावट आए, कोड बेस की जटिलता बढ़ जाए वगैरह. इस ब्लॉग पोस्ट में आपको ऑप्टिमाइज़ेशन के लिए काम के जितने भी विकल्प दिख रहे हैं, उनके अलावा ऐसे अनगिनत विकल्प हैं जो काम नहीं कर पाए.

अगर आपकी स्थिति भी ऐसी ही है, तो यह अनुमान लगाने की कोशिश करें कि कम से कम काम करके, मेट्रिक को कितना बेहतर बनाया जा सकता है. इसका मतलब है कि क्रम से:

  1. पहले से इकट्ठा की गई मेट्रिक या सिर्फ़ अपने अनुभव के आधार पर अनुमान लगाना
  2. जल्दबाज़ी में और बिना किसी खास तरीके के बनाए गए प्रोटोटाइप के आधार पर अनुमान लगाना
  3. कोई समाधान लागू करना.

अपने समाधान की कमियों का अनुमान लगाना न भूलें. उदाहरण के लिए, अगर आपको अतिरिक्त डेटा स्ट्रक्चर पर निर्भर रहना है, तो आप कितनी मेमोरी का इस्तेमाल करना चाहेंगे?

समस्या को गहराई से समझना

अब हम उन बदलावों के बारे में बात करेंगे जिन्हें हमने लागू किया है.

हमने FindReferenceInfoOf नाम के एक तरीके को ऑप्टिमाइज़ करने के लिए, एक बदलाव लागू किया है. यह तरीका, किसी एंट्री को ढूंढने के लिए वेक्टर की लीनियर खोज कर रहा था. हमने उस डेटा स्ट्रक्चर को निर्देश के आईडी के हिसाब से इंडेक्स करने के लिए अपडेट किया, ताकि FindReferenceInfoOf, O(n) के बजाय O(1) हो जाए. इसके अलावा, हमने वेक्टर को पहले से ही एलोकेट कर दिया, ताकि उसका साइज़ न बदलना पड़े. हमने मेमोरी को थोड़ा बढ़ाया, क्योंकि हमें एक अतिरिक्त फ़ील्ड जोड़ना पड़ा. इससे यह गिना जाता था कि हमने वेक्टर में कितनी एंट्री डाली हैं. हालांकि, यह एक छोटा सा बदलाव था, क्योंकि पीक मेमोरी नहीं बढ़ी. इससे, LoadStoreAnalysis फ़ेज़ की स्पीड 34-66% बढ़ गई. इससे कंपाइल करने में लगने वाले समय में ~0.5-1.8% का सुधार हुआ.

हमारे पास HashSet का एक कस्टम वर्शन है, जिसका इस्तेमाल हम कई जगहों पर करते हैं. इस डेटा स्ट्रक्चर को बनाने में काफ़ी समय लग रहा था. हमें इसकी वजह पता चली. कई साल पहले, इस डेटा स्ट्रक्चर का इस्तेमाल सिर्फ़ कुछ जगहों पर किया जाता था. इन जगहों पर बहुत बड़े HashSets का इस्तेमाल किया जाता था. इसे ऑप्टिमाइज़ करने के लिए, इसमें बदलाव किया गया था. हालांकि, आजकल इसका इस्तेमाल सिर्फ़ कुछ एंट्री और कम समय के लिए किया जाता था. इसका मतलब है कि हम इस बड़े HashSet को बनाकर साइकल बर्बाद कर रहे थे. हालांकि, हमने इसे कुछ एंट्री के लिए ही इस्तेमाल किया और फिर छोड़ दिया. इस बदलाव से, हमने कंपाइल करने में लगने वाले समय में ~1.3-2% का सुधार किया. इसके अलावा, मेमोरी का इस्तेमाल ~0.5-1% कम हो गया, क्योंकि हम पहले की तरह बड़े डेटा स्ट्रक्चर का इस्तेमाल नहीं कर रहे थे.

हमने डेटा स्ट्रक्चर को कॉपी करने से बचने के लिए, उन्हें रेफ़रंस के तौर पर लैम्डा को पास करके, कंपाइल करने में लगने वाले समय में ~0.5-1% का सुधार किया. यह एक ऐसी चीज़ थी जिसे ओरिजनल समीक्षा में अनदेखा कर दिया गया था और यह हमारे कोडबेस में कई सालों तक मौजूद रही. pprof में प्रोफ़ाइल देखने के बाद, हमें पता चला कि ये तरीके बहुत सारे डेटा स्ट्रक्चर बना और मिटा रहे थे. इससे हमें इनकी जांच करने और इन्हें ऑप्टिमाइज़ करने में मदद मिली.

हमने कंपाइल किए गए आउटपुट को लिखने वाले फ़ेज़ की स्पीड बढ़ाई. इसके लिए, कंपाइल की गई वैल्यू को कैश किया गया. इससे कंपाइल करने में लगने वाले कुल समय में ~1.3-2.8% का सुधार हुआ. माफ़ करें, अतिरिक्त बुककीपिंग बहुत ज़्यादा थी और हमारे ऑटोमेटेड टेस्टिंग ने हमें मेमोरी से जुड़ी समस्या के बारे में अलर्ट किया. बाद में, हमने उसी कोड को फिर से देखा और एक नया वर्शन लागू किया. इससे न सिर्फ़ मेमोरी से जुड़ी समस्या ठीक हुई, बल्कि कंपाइल करने में लगने वाले समय में ~0.5-1.8% का और सुधार हुआ! इस दूसरे बदलाव में, हमें दो डेटा स्ट्रक्चर में से एक को हटाने के लिए, इस फ़ेज़ को फिर से फ़ैक्टर करना पड़ा और यह सोचना पड़ा कि यह फ़ेज़ कैसे काम करना चाहिए.

हमारे ऑप्टिमाइज़ करने वाले कंपाइलर में, एक फ़ेज़ होता है. यह बेहतर परफ़ॉर्मेंस पाने के लिए, फ़ंक्शन कॉल को इनलाइन करता है. इनलाइन करने के लिए, हम किसी भी कंपाइलिंग से पहले ह्यूरिस्टिक्स और काम करने के बाद, लेकिन इनलाइनिंग को फ़ाइनल करने से ठीक पहले, फ़ाइनल जांच, दोनों का इस्तेमाल करते हैं. अगर इनमें से किसी में भी यह पता चलता है कि इनलाइनिंग काम की नहीं है (उदाहरण के लिए, बहुत सारे नए निर्देश जोड़े जाएंगे), तो हम मेथड कॉल को इनलाइन नहीं करते.

हमने दो जांचों को “फ़ाइनल जांच” कैटगरी से “ह्यूरिस्टिक” कैटगरी में ले जाया, ताकि समय लेने वाली कंपाइलिंग से पहले यह अनुमान लगाया जा सके कि इनलाइनिंग सफल होगी या नहीं. यह अनुमान है, इसलिए यह पूरी तरह से सटीक नहीं है. हालांकि, हमने पुष्टि की है कि हमारे नए ह्यूरिस्टिक्स, पहले इनलाइन किए गए 99.9% कॉन्टेंट को कवर करते हैं. साथ ही, इससे परफ़ॉर्मेंस पर कोई असर नहीं पड़ता. इन नए ह्यूरिस्टिक्स में से एक, ज़रूरी DEX रजिस्टर (~0.2-1.3% सुधार) के बारे में था. दूसरा, निर्देशों की संख्या (~2% सुधार) के बारे में था.

हमारे पास BitVector का एक कस्टम वर्शन है, जिसका इस्तेमाल हम कई जगहों पर करते हैं. हमने कुछ तय साइज़ वाले बिट वेक्टर के लिए, साइज़ बदलने वाले BitVector क्लास को, आसान BitVectorView से बदल दिया है. इससे कुछ इनडायरेक्शन और रन-टाइम रेंज की जांच खत्म हो जाती है. साथ ही, बिट वेक्टर ऑब्जेक्ट के कंस्ट्रक्शन की स्पीड बढ़ जाती है.

इसके अलावा, BitVectorView क्लास को, पुराने BitVector की तरह हमेशा uint32_t का इस्तेमाल करने के बजाय, स्टोरेज के टाइप के हिसाब से टेंप्लेट किया गया था. इससे कुछ कार्रवाइयां, जैसे कि Union(), 64-बिट प्लैटफ़ॉर्म पर एक साथ दोगुने बिट प्रोसेस कर सकती हैं. Android OS को कंपाइल करते समय, असर डालने वाले फ़ंक्शन के सैंपल कुल मिलाकर 1% से ज़्यादा कम हो गए. यह कई बदलावों के ज़रिए किया गया [123456]

अगर हम सभी ऑप्टिमाइज़ेशन के बारे में विस्तार से बात करेंगे, तो हमें पूरा दिन लग जाएगा! अगर आपको कुछ और ऑप्टिमाइज़ेशन के बारे में जानना है, तो हमने लागू किए गए कुछ अन्य बदलाव देखें:

नतीजा

ART के कंपाइल करने में लगने वाले समय की स्पीड को बेहतर बनाने की हमारी कोशिशों से, काफ़ी सुधार हुए हैं. इससे Android ज़्यादा फ़्लूइड और बेहतर हुआ है. साथ ही, बैटरी लाइफ़ और डिवाइस के तापमान को बेहतर बनाने में भी मदद मिली है. ऑप्टिमाइज़ेशन के लिए काम के विकल्पों की पहचान करके और उन्हें लागू करके, हमने दिखाया है कि मेमोरी के इस्तेमाल या कोड की क्वालिटी से समझौता किए बिना, कंपाइल करने में लगने वाले समय को काफ़ी कम किया जा सकता है.

हमारी इस कोशिश में, pprof जैसे टूल की मदद से प्रोफ़ाइलिंग करना, बार-बार बदलाव करने की इच्छा, और कभी-कभी कम फ़ायदे वाले तरीकों को छोड़ना शामिल था. ART की टीम की सामूहिक कोशिशों से, कंपाइल करने में लगने वाले समय को काफ़ी कम किया गया है. साथ ही, आने वाले समय में होने वाले सुधारों के लिए भी आधार तैयार किया गया है.

ये सभी सुधार, साल 2025 के आखिर में Android के अपडेट में उपलब्ध हैं. साथ ही, Android 12 और उसके बाद के वर्शन के लिए, मेनलाइन अपडेट के ज़रिए भी उपलब्ध हैं. हमें उम्मीद है कि ऑप्टिमाइज़ेशन की हमारी प्रोसेस के बारे में यह जानकारी, कंपाइलर इंजीनियरिंग की जटिलताओं और फ़ायदों के बारे में अहम जानकारी देगी!

लेखक:

पढ़ना जारी रखें