वर्शन 8 में JavaScript की परफ़ॉर्मेंस से जुड़ी सलाह

Chris Wilson
Chris Wilson

परिचय

डेनियल क्लिफ़र्ड ने 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 से शुरू होने वाली लगातार कुंजियों का इस्तेमाल करें
  • बड़े ऐरे (जैसे, 64 हज़ार से ज़्यादा एलिमेंट) को उनके ज़्यादा से ज़्यादा साइज़ के लिए पहले से तय न करें. इसके बजाय, ज़रूरत के हिसाब से उन्हें बढ़ाएं
  • अरे में मौजूद एलिमेंट न मिटाएं. खास तौर पर, न्यूमेरिक अरे में मौजूद एलिमेंट
  • ऐसे एलिमेंट लोड न करें जिन्हें शुरू नहीं किया गया है या जिन्हें मिटा दिया गया है:
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 (जेआईटी) कंपाइलर होते हैं:

  • "फ़ुल" कंपाइलर, जो किसी भी 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

(इससे ऑप्टिमाइज़ किए गए फ़ंक्शन के नाम, stdout में लॉग होते हैं.)

हालांकि, सभी फ़ंक्शन ऑप्टिमाइज़ नहीं किए जा सकते. कुछ सुविधाएं, ऑप्टिमाइज़ करने वाले कंपाइलर को किसी फ़ंक्शन पर चलने से रोकती हैं. इसे "बेल-आउट" कहा जाता है. खास तौर पर, ऑप्टिमाइज़ करने वाला कंपाइलर फ़िलहाल 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 इंजन आपके कोड के साथ कैसे काम करता है. एक बार फिर, बुनियादी सलाह यह है:

  • समस्या आने (या दिखने) से पहले ही तैयार रहें
  • इसके बाद, अपनी समस्या की मुख्य वजह को पहचानें और समझें
  • आखिर में, ज़रूरी चीज़ों को ठीक करें

इसका मतलब है कि आपको यह पक्का करना चाहिए कि समस्या आपकी JavaScript में है. इसके लिए, पहले PageSpeed जैसे अन्य टूल का इस्तेमाल करें. मेट्रिक इकट्ठा करने से पहले, हो सकता है कि आपको JavaScript (कोई DOM नहीं) को कम करना पड़े. इसके बाद, उन मेट्रिक का इस्तेमाल करके रुकावटों का पता लगाएं और ज़रूरी रुकावटों को हटाएं. हमें उम्मीद है कि डैनियल की बातचीत (और इस लेख) से आपको यह समझने में मदद मिलेगी कि V8, JavaScript को कैसे चलाता है. हालांकि, अपने एल्गोरिदम को ऑप्टिमाइज़ करने पर भी ध्यान दें!

रेफ़रंस