परिचय
डेनियल क्लिफ़र्ड ने Google I/O में V8 में JavaScript की परफ़ॉर्मेंस को बेहतर बनाने के बारे में सलाह और तरकीबें दीं. डेनियल ने हमें "ज़्यादा तेज़ी से काम करने की मांग करने" के लिए कहा. इसका मतलब है कि C++ और JavaScript के बीच परफ़ॉर्मेंस में अंतर का ध्यान से विश्लेषण करना और JavaScript के काम करने के तरीके को ध्यान में रखकर कोड लिखना. इस लेख में, डेनियल के टॉक में बताए गए सबसे अहम पॉइंट की खास जानकारी दी गई है. साथ ही, परफ़ॉर्मेंस से जुड़े दिशा-निर्देशों में बदलाव होने पर, हम इस लेख को भी अपडेट करते रहेंगे.
सबसे अहम सलाह
परफ़ॉर्मेंस से जुड़ी किसी भी सलाह को संदर्भ में रखना ज़रूरी है. परफ़ॉर्मेंस से जुड़ी सलाह काफ़ी दिलचस्प होती है. कभी-कभी, सबसे पहले ज़्यादा जानकारी वाली सलाह पर ध्यान देने से, असल समस्याओं पर ध्यान नहीं दिया जा पाता. आपको अपने वेब ऐप्लिकेशन की परफ़ॉर्मेंस का पूरा विश्लेषण करना होगा. परफ़ॉर्मेंस से जुड़े इन सुझावों पर ध्यान देने से पहले, आपको PageSpeed जैसे टूल की मदद से अपने कोड का विश्लेषण करना चाहिए और अपना स्कोर बढ़ाना चाहिए. इससे, आपको समय से पहले ऑप्टिमाइज़ेशन से बचने में मदद मिलेगी.
वेब ऐप्लिकेशन में अच्छी परफ़ॉर्मेंस पाने के लिए, सबसे सही बुनियादी सलाह यह है:
- समस्या होने या उसके बारे में पता चलने से पहले तैयार रहें
- इसके बाद, अपनी समस्या की मुख्य वजह की पहचान करें और उसे समझें
- आखिर में, ज़रूरी समस्याओं को ठीक करें
इन चरणों को पूरा करने के लिए, यह समझना ज़रूरी हो सकता है कि V8, JS को कैसे ऑप्टिमाइज़ करता है. इससे, JS के रनटाइम डिज़ाइन को ध्यान में रखकर कोड लिखा जा सकता है. यह भी ज़रूरी है कि आप उपलब्ध टूल के बारे में जानें और यह भी जानें कि वे आपकी मदद कैसे कर सकते हैं. डेवलपर टूल इस्तेमाल करने के तरीके के बारे में ज़्यादा जानकारी के लिए, डेनियल के टॉक को देखें. इस दस्तावेज़ में, V8 इंजन के डिज़ाइन के कुछ सबसे अहम पॉइंट के बारे में बताया गया है.
अब V8 के बारे में सलाह पर आते हैं!
छिपी हुई क्लास
JavaScript में, कंपाइल के समय टाइप की सीमित जानकारी होती है: टाइप को रनटाइम पर बदला जा सकता है. इसलिए, यह उम्मीद करना स्वाभाविक है कि कंपाइल के समय JS टाइप के बारे में सोचना महंगा हो. इससे आपको यह सवाल पूछने को मजबूर होना पड़ सकता है कि JavaScript की परफ़ॉर्मेंस, C++ के आस-पास कैसे हो सकती है. हालांकि, V8 में रनटाइम के दौरान ऑब्जेक्ट के लिए, अंदरूनी तौर पर छिपे हुए टाइप बनाए जाते हैं. इसके बाद, एक ही छिपी हुई क्लास वाले ऑब्जेक्ट, ऑप्टिमाइज़ किए गए जनरेट किए गए कोड का इस्तेमाल कर सकते हैं.
उदाहरण के लिए:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!```
जब तक ऑब्जेक्ट इंस्टेंस p2 में अतिरिक्त सदस्य ".z" नहीं जोड़ा जाता, तब तक p1 और p2 में एक ही छिपी हुई क्लास होती है. इसलिए, V8, JavaScript कोड के लिए ऑप्टिमाइज़ की गई असेंबली का एक वर्शन जनरेट कर सकता है, जो p1 या p2 में से किसी एक में बदलाव करता है. छिपी हुई क्लास को अलग-अलग करने से जितना ज़्यादा बचा जा सकता है, उतनी ही बेहतर परफ़ॉर्मेंस मिलेगी.
इसलिए
- कन्स्ट्रक्टर फ़ंक्शन में सभी ऑब्जेक्ट के सदस्यों को शुरू करना (ताकि बाद में इंस्टेंस का टाइप न बदले)
- ऑब्जेक्ट के सदस्यों को हमेशा एक ही क्रम में शुरू करें
Numbers
V8, वैल्यू को बेहतर तरीके से दिखाने के लिए टैगिंग का इस्तेमाल करता है. ऐसा तब किया जाता है, जब वैल्यू के टाइप में बदलाव हो सकता है. V8, इस्तेमाल की गई वैल्यू से यह अनुमान लगाता है कि किस तरह के नंबर का इस्तेमाल किया जा रहा है. V8 के यह अनुमान लगाने के बाद, वह वैल्यू को बेहतर तरीके से दिखाने के लिए टैगिंग का इस्तेमाल करता है. ऐसा इसलिए, क्योंकि ये टाइप डाइनैमिक तौर पर बदल सकते हैं. हालांकि, इन टाइप टैग को बदलने पर कभी-कभी लागत आती है. इसलिए, नंबर टाइप का लगातार इस्तेमाल करना सबसे अच्छा होता है. आम तौर पर, जहां ज़रूरी हो वहां 31-बिट वाले साइन वाले इंटिजर का इस्तेमाल करना सबसे सही होता है.
उदाहरण के लिए:
var i = 42; // this is a 31-bit signed integer
var j = 4.2; // this is a double-precision floating point number```
इसलिए
- ऐसी संख्यात्मक वैल्यू का इस्तेमाल करें जिन्हें 31-बिट के साइन वाले पूर्णांक के तौर पर दिखाया जा सकता है.
श्रेणियां
बड़े और कम एलिमेंट वाले ऐरे को मैनेज करने के लिए, इंटरनल तौर पर दो तरह के ऐरे स्टोरेज होते हैं:
- फ़ास्ट एलिमेंट: कॉम्पैक्ट पासकोड सेट के लिए लीनियर स्टोरेज
- डिक्शनरी एलिमेंट: हैश टेबल का स्टोरेज
अरे का स्टोरेज एक टाइप से दूसरे टाइप में न बदले, यह पक्का करना सबसे बेहतर है.
इसलिए
- ऐरे के लिए, 0 से शुरू होने वाली लगातार बटन का इस्तेमाल करना
- बड़े ऐरे (जैसे, 64K से ज़्यादा एलिमेंट) को उनके ज़्यादा से ज़्यादा साइज़ के लिए पहले से न तय करें. इसके बजाय, ज़रूरत के हिसाब से साइज़ बढ़ाएं
- ऐरे में मौजूद एलिमेंट, खास तौर पर अंकों वाले ऐरे को न मिटाएं
- शुरू नहीं किए गए या मिटाए गए एलिमेंट लोड न करें:
for (var b = 0; b < 10; b++) {
a[0] |= b; // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
a[0] |= b; // Much better! 2x faster.
}
साथ ही, डबल्स के ऐरे तेज़ी से काम करते हैं - ऐरे की छिपी हुई क्लास, एलिमेंट टाइप को ट्रैक करती है. साथ ही, सिर्फ़ डबल्स वाले ऐरे को अनबॉक्स किया जाता है (इससे छिपी हुई क्लास में बदलाव होता है). हालांकि, ऐरे में लापरवाही से बदलाव करने पर, बॉक्सिंग और अनबॉक्सिंग की वजह से ज़्यादा काम करना पड़ सकता है - उदाहरण के लिए,
var a = new Array();
a[0] = 77; // Allocates
a[1] = 88;
a[2] = 0.5; // Allocates, converts
a[3] = true; // Allocates, converts```
इनसे कम असरदार है:
var a = [77, 88, 0.5, true];
क्योंकि पहले उदाहरण में, अलग-अलग असाइनमेंट एक के बाद एक किए जाते हैं. साथ ही, a[2]
के असाइनमेंट की वजह से, ऐरे को अनबॉक्स किए गए डबल के ऐरे में बदल दिया जाता है. हालांकि, a[3]
के असाइनमेंट की वजह से, इसे फिर से उस ऐरे में बदल दिया जाता है जिसमें कोई भी वैल्यू (संख्या या ऑब्जेक्ट) हो सकती है. दूसरे मामले में, कंपाइलर को लिटरल में मौजूद सभी एलिमेंट के टाइप की जानकारी होती है. साथ ही, छिपी हुई क्लास का पता पहले से लगाया जा सकता है.
- तय साइज़ वाले छोटे ऐरे के लिए, ऐरे लिटरल का इस्तेमाल करके शुरू करना
- छोटे ऐरे (<64k) का इस्तेमाल करने से पहले, उनके साइज़ को सही करने के लिए उन्हें पहले से तय करें
- संख्याओं वाले ऐरे में, संख्याओं के अलावा अन्य वैल्यू (ऑब्जेक्ट) सेव न करें
- अगर लिटरल के बिना शुरू किया जाता है, तो छोटे ऐरे को फिर से बदलने से बचें.
JavaScript कंपाइलेशन
JavaScript एक बहुत ही डाइनैमिक भाषा है और इसे मूल रूप से इंटरप्रिटर के ज़रिए लागू किया जाता था. हालांकि, आधुनिक JavaScript रनटाइम इंजन, कंपाइलेशन का इस्तेमाल करते हैं. असल में, V8 (Chrome का JavaScript) में दो अलग-अलग Just-In-Time (JIT) कंपाइलर होते हैं:
- "पूरा" कंपाइलर, जो किसी भी JavaScript के लिए अच्छा कोड जनरेट कर सकता है
- ऑप्टिमाइज़ करने वाला कंपाइलर, जो ज़्यादातर JavaScript के लिए बेहतरीन कोड बनाता है. हालांकि, इसे कंपाइल करने में ज़्यादा समय लगता है.
The Full Compiler
V8 में, फ़ुल कंपाइलर सभी कोड पर काम करता है और जल्द से जल्द कोड को लागू करना शुरू कर देता है. इससे, अच्छा कोड तुरंत जनरेट होता है, लेकिन बेहतर कोड नहीं. यह कंपाइलर, कंपाइल करने के समय वैरिएबल के टाइप के बारे में कुछ नहीं मानता. यह उम्मीद करता है कि वैरिएबल के टाइप, रनटाइम के दौरान बदल सकते हैं और बदलेंगे. फ़ुल कंपाइलर से जनरेट किया गया कोड, प्रोग्राम के चलने के दौरान टाइप के बारे में जानकारी को बेहतर बनाने के लिए, इनलाइन कैश (आईसी) का इस्तेमाल करता है. इससे प्रोग्राम की परफ़ॉर्मेंस बेहतर होती है.
इनलाइन कैश मेमोरी का मकसद, ऑपरेशन के लिए टाइप पर निर्भर कोड को कैश मेमोरी में सेव करके, टाइप को बेहतर तरीके से मैनेज करना है. जब कोड चलता है, तो वह पहले टाइप की मान्यता की पुष्टि करता है. इसके बाद, ऑपरेशन को कम समय में पूरा करने के लिए इनलाइन कैश मेमोरी का इस्तेमाल करता है. हालांकि, इसका मतलब है कि एक से ज़्यादा टाइप स्वीकार करने वाले ऑपरेशन की परफ़ॉर्मेंस खराब होगी.
इसलिए
- पॉलीमोर्फ़िक ऑपरेशन के बजाय, ऑपरेशन के मोनोमॉर्फ़िक इस्तेमाल को प्राथमिकता दी जाती है
अगर इनपुट की छिपी हुई क्लास हमेशा एक जैसी होती हैं, तो ऑपरेशन को मोनोमॉर्फ़िक कहा जाता है. अगर ऐसा नहीं होता है, तो उन्हें पॉलीमॉर्फ़िक कहा जाता है. इसका मतलब है कि ऑपरेशन के अलग-अलग कॉल में कुछ आर्ग्युमेंट का टाइप बदल सकता है. उदाहरण के लिए, इस उदाहरण में दूसरे add() कॉल की वजह से पॉलीमरफ़िज़्म होता है:
function add(x, y) {
return x + y;
}
add(1, 2); // + in add is monomorphic
add("a", "b"); // + in add becomes polymorphic```
ऑप्टिमाइज़ करने वाला कंपाइलर
V8, फ़ुल कंपाइलर के साथ-साथ, ऑप्टिमाइज़ करने वाले कंपाइलर की मदद से "हॉट" फ़ंक्शन (यानी, कई बार चलने वाले फ़ंक्शन) को फिर से कंपाइल करता है. यह कंपाइलर, टाइप फ़ीडबैक का इस्तेमाल करके, कंपाइल किए गए कोड को तेज़ी से बनाता है. असल में, यह उन आईसी से लिए गए टाइप का इस्तेमाल करता है जिनके बारे में हमने अभी बात की है!
ऑप्टिमाइज़ करने वाले कंपाइलर में, ऑपरेशन को अनुमानित तौर पर इनलाइन किया जाता है (जहां उन्हें कॉल किया जाता है वहां सीधे तौर पर रखा जाता है). इससे, मेमोरी फ़ुटप्रिंट की कीमत पर, प्रोसेस को तेज़ी से पूरा किया जा सकता है. साथ ही, अन्य ऑप्टिमाइज़ेशन भी चालू किए जा सकते हैं. मोनोमॉर्फ़िक फ़ंक्शन और कंस्ट्रक्टर को पूरी तरह से इनलाइन किया जा सकता है. यही वजह है कि V8 में मोनोमॉर्फ़िज्म का इस्तेमाल करना अच्छा है.
V8 इंजन के स्टैंडअलोन "d8" वर्शन का इस्तेमाल करके, यह लॉग किया जा सकता है कि क्या ऑप्टिमाइज़ किया गया है:
d8 --trace-opt primes.js
(यह ऑप्टिमाइज़ किए गए फ़ंक्शन के नामों को स्टैंडर्ड आउटपुट में लॉग करता है.)
सभी फ़ंक्शन ऑप्टिमाइज़ नहीं किए जा सकते. हालांकि, कुछ सुविधाएं ऑप्टिमाइज़ करने वाले कंपाइलर को किसी फ़ंक्शन पर चलने से रोकती हैं. इसे "बैल-आउट" कहा जाता है. खास तौर पर, ऑप्टिमाइज़ करने वाला कंपाइलर फ़िलहाल try {} catch {} ब्लॉक वाले फ़ंक्शन पर काम नहीं करता!
इसलिए
- अगर आपके पास try {} catch {} ब्लॉक हैं, तो परफ़ॉर्मेंस पर असर डालने वाले कोड को नेस्ट किए गए फ़ंक्शन में डालें: ```js function perf_sensitive() { // Do performance-sensitive work here }
try { perf_sensitive() } catch (e) { // Handle exceptions here } ```
आने वाले समय में, इस दिशा-निर्देश में बदलाव हो सकता है. ऐसा इसलिए, क्योंकि हम ऑप्टिमाइज़ करने वाले कंपाइलर में try/catch ब्लॉक चालू करते हैं. ऊपर बताए गए तरीके से d8 के साथ "--trace-opt" विकल्प का इस्तेमाल करके, यह जांच की जा सकती है कि ऑप्टिमाइज़ करने वाला कंपाइलर, फ़ंक्शन को कैसे बाहर निकाल रहा है. इससे आपको यह जानकारी मिलती है कि किन फ़ंक्शन को बाहर निकाला गया था:
d8 --trace-opt primes.js
ऑप्टिमाइज़ेशन हटाना
आखिर में, इस कंपाइलर से किया गया ऑप्टिमाइज़ेशन अनुमानित होता है - कभी-कभी यह काम नहीं करता और हम पीछे हट जाते हैं. "डिऑप्टिमाइज़ेशन" की प्रोसेस, ऑप्टिमाइज़ किए गए कोड को हटा देती है और "पूरे" कंपाइलर कोड में सही जगह पर फिर से प्रोसेस शुरू कर देती है. रीऑप्टिमाइज़ेशन बाद में फिर से ट्रिगर हो सकता है. हालांकि, कुछ समय के लिए, रीऑप्टिमाइज़ेशन की प्रोसेस धीमी हो जाती है. खास तौर पर, फ़ंक्शन ऑप्टिमाइज़ होने के बाद, वैरिएबल की छिपी हुई क्लास में बदलाव करने से, यह डीऑप्टिमाइज़ेशन होगा.
इसलिए
- फ़ंक्शन को ऑप्टिमाइज़ करने के बाद, उनमें छिपी हुई क्लास में बदलाव करने से बचें
अन्य ऑप्टिमाइज़ेशन की तरह ही, आपको उन फ़ंक्शन का लॉग मिल सकता है जिन्हें V8 को लॉगिंग फ़्लैग की मदद से डीऑप्टिमाइज़ करना पड़ा था:
d8 --trace-deopt primes.js
V8 के अन्य टूल
वैसे, Chrome के स्टार्टअप पर V8 ट्रैकिंग के विकल्प भी पास किए जा सकते हैं:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```
डेवलपर टूल की प्रोफ़ाइलिंग का इस्तेमाल करने के अलावा, प्रोफ़ाइलिंग के लिए d8 का भी इस्तेमाल किया जा सकता है:
% out/ia32.release/d8 primes.js --prof
यह सुविधा, पहले से मौजूद सैंपलिंग प्रोफ़ाइलर का इस्तेमाल करती है. यह हर मिलीसेकंड में एक सैंपल लेती है और v8.log लिखती है.
खास जानकारी
यह पहचानना और समझना ज़रूरी है कि बेहतर परफ़ॉर्म करने वाला JavaScript बनाने के लिए, V8 इंजन आपके कोड के साथ कैसे काम करता है. एक बार फिर, बुनियादी सलाह यह है:
- समस्या होने या उसके बारे में पता चलने से पहले तैयार रहें
- इसके बाद, अपनी समस्या की मुख्य वजह की पहचान करें और उसे समझें
- आखिर में, ज़रूरी समस्याओं को ठीक करें
इसका मतलब है कि आपको सबसे पहले PageSpeed जैसे दूसरे टूल का इस्तेमाल करके, यह पक्का करना चाहिए कि समस्या आपके JavaScript में है. इसके बाद, मेट्रिक इकट्ठा करने से पहले, शायद आपको सिर्फ़ JavaScript (कोई DOM नहीं) का इस्तेमाल करना पड़े. इसके बाद, उन मेट्रिक का इस्तेमाल करके, समस्याओं का पता लगाएं और ज़रूरी समस्याओं को ठीक करें. हमें उम्मीद है कि डैनियल के इस टॉक और लेख से आपको यह समझने में मदद मिलेगी कि V8, JavaScript को कैसे चलाता है. हालांकि, अपने एल्गोरिदम को ऑप्टिमाइज़ करने पर भी ध्यान दें!