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

Chris Wilson
Chris Wilson

शुरुआती जानकारी

डैनियल क्लिफ़र्ड ने वर्शन 8 में JavaScript की परफ़ॉर्मेंस को बेहतर बनाने के लिए, सुझाव और तरकीबों के बारे में Google I/O में शानदार चर्चा की. डेनियल ने हमें 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 में बदलाव करता है. छिपी हुई क्लास के अलग-अलग वर्शन बनने से जितना ज़्यादा बचा जा सकता है, आपको उतना ही बेहतर परफ़ॉर्मेंस मिलेगी.

इसलिए

  • कंस्ट्रक्टर फ़ंक्शन में ऑब्जेक्ट के सभी सदस्यों को शुरू करें (ताकि बाद में इंस्टेंस टाइप न बदलें)
  • ऑब्जेक्ट के सदस्यों को हमेशा उसी क्रम में शुरू करें

संख्याएं

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) कंपाइलर हैं:

  • "Full" कंपाइलर, जो किसी भी JavaScript के लिए अच्छा कोड जनरेट कर सकता है
  • ऑप्टिमाइज़िंग कंपाइलर, जो ज़्यादातर JavaScript के लिए बढ़िया कोड बनाता है, लेकिन कंपाइल करने में ज़्यादा समय लगता है.

पूरा कंपाइलर

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

इनलाइन कैश मेमोरी का मकसद डेटा के टाइप को कैश मेमोरी में सेव करना है, ताकि वह अलग-अलग तरह के डेटा को आसानी से प्रोसेस कर सके. जब कोड चलता है, तो सबसे पहले वह डेटा के टाइप से जुड़े अनुमानों की पुष्टि करता है. इसके बाद, कार्रवाई को शॉर्टकट करने के लिए इनलाइन कैश मेमोरी का इस्तेमाल करता है. हालांकि, इसका मतलब है कि कई तरह की कार्रवाइयों को स्वीकार करने वाली कार्रवाइयों की परफ़ॉर्मेंस कम होगी.

इसलिए

  • पॉलीमॉर्फ़िक संक्रियाओं की तुलना में मोनोमॉर्फ़िक संक्रियाओं को प्राथमिकता दें

अगर इनपुट के छिपे हुए क्लास हमेशा एक जैसे होते हैं, तो ऑपरेशन मोनोमॉर्फ़िक होते हैं - नहीं तो वे पॉलीमॉर्फ़िक होते हैं. इसका मतलब है कि कुछ आर्ग्युमेंट, ऑपरेशन के लिए अलग-अलग कॉल में टाइप बदल सकते हैं. उदाहरण के लिए, इस उदाहरण में दूसरे add() कॉल की वजह से पॉलीमॉर्फ़िज़्म होता है:

function add(x, y) {
  return x + y;
}

add(1, 2);      // + in add is monomorphic
add("a", "b");  // + in add becomes polymorphic```

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

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

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

V8 इंजन के स्टैंडअलोन "d8" वर्शन का इस्तेमाल करके, ऑप्टिमाइज़ की जाने वाली चीज़ों को लॉग किया जा सकता है:

d8 --trace-opt primes.js

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

सभी फ़ंक्शन को ऑप्टिमाइज़ नहीं किया जा सकता. हालांकि, कुछ सुविधाएं, ऑप्टिमाइज़ करने वाले कंपाइलर को दिए गए फ़ंक्शन ("बैल-आउट") पर चलने से रोकती हैं. खास तौर पर, ऑप्टिमाइज़ करने वाला कंपाइलर फ़िलहाल {}कैच {} ब्लॉक करके फ़ंक्शन की मदद से सुरक्षा करता है!

इसलिए

  • अगर आपने {}hidden {} block: ```js फ़ंक्शन perf_sensitive() { // परफ़ॉर्मेंस के हिसाब से काम करें यहां करें }

कोशिश करें { perf_sensitive() } कैच (e) { // अपवादों को यहां हैंडल करें } ```

आने वाले समय में, इस दिशा-निर्देश में बदलाव हो सकता है. इसकी वजह यह है कि हम ऑप्टिमाइज़ेशन कंपाइलर में 'ट्राई/कैच ब्लॉक' सुविधा को चालू करते हैं. यह पता लगाया जा सकता है कि ऑप्टिमाइज़ करने वाला कंपाइलर, फ़ंक्शन को कैसे बचा रहा है. इसके लिए, ऊपर बताए गए 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 को घटाकर (कोई डीओएम नहीं) करें. इसके बाद, उन मेट्रिक का इस्तेमाल करके रुकावटों का पता लगाएं और ज़रूरी गड़बड़ियों को खत्म करें. उम्मीद है कि डेनियल की बात (और इस लेख) से आपको यह बेहतर ढंग से समझने में मदद मिलेगी कि V8 JavaScript कैसे चलाता है - साथ ही, आप अपने एल्गोरिदम को ऑप्टिमाइज़ करने पर भी ध्यान दें!

रेफ़रंस