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

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

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

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

इसे लिखा है:

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