परिचय
डेनियल क्लिफ़र्ड ने 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, p1 या p2 में बदलाव करने वाले JavaScript कोड के लिए ऑप्टिमाइज़ किए गए असेंबली का सिंगल वर्शन जनरेट कर सकता है. छिपी हुई क्लास को अलग-अलग करने से जितना ज़्यादा बचा जा सकता है, उतनी ही बेहतर परफ़ॉर्मेंस मिलेगी.
इसलिए
- कन्स्ट्रक्टर फ़ंक्शन में सभी ऑब्जेक्ट के सदस्यों को शुरू करना (ताकि बाद में इंस्टेंस का टाइप न बदले)
- ऑब्जेक्ट के सदस्यों को हमेशा एक ही क्रम में शुरू करें
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 के लिए बेहतर कोड बनाता है. हालांकि, इसे कंपाइल करने में ज़्यादा समय लगता है.
द फ़ुल कंपाइलर
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
(यह ऑप्टिमाइज़ किए गए फ़ंक्शन के नामों को स्टैंडर्ड आउटपुट में लॉग करता है.)
सभी फ़ंक्शन को ऑप्टिमाइज़ नहीं किया जा सकता. हालांकि, कुछ सुविधाएं ऑप्टिमाइज़ करने वाले कंपाइलर को किसी फ़ंक्शन पर चलने से रोकती हैं. इसे "बैल-आउट" कहा जाता है. खास तौर पर, ऑप्टिमाइज़ करने वाला कंपाइलर फ़िलहाल कोशिश {}कैच {} ब्लॉक वाले फ़ंक्शन को ही विफल कर देता है!
इसलिए
- अगर आपने {} create {} blocked: ```js action perf_sensitive() { // Do Performance- अपग्रेड here }
आज़माएं { perf_sensitive() } शानदार तरीके से (e) { // Handle अपवाद 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 को कैसे चलाता है. हालांकि, अपने एल्गोरिदम को ऑप्टिमाइज़ करने पर भी ध्यान दें!