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

Chris Wilson
Chris Wilson

परिचय

डेनियल क्लिफ़र्ड ने V8 में JavaScript की परफ़ॉर्मेंस को बेहतर बनाने के लिए Google I/O में शानदार तरीके से सुझाव और तरकीबें बताईं. डैनियल ने हमें "तेज़ी से मांग" करने के लिए बढ़ावा दिया - का इस्तेमाल किया जा सकता है. इस लेख में डैनियल की बातचीत के सबसे अहम पॉइंट के बारे में बताया गया है. साथ ही, परफ़ॉर्मेंस से जुड़े दिशा-निर्देशों में बदलाव होने पर, हम इस लेख को भी अपडेट करते रहेंगे.

सबसे ज़रूरी सलाह

परफ़ॉर्मेंस से जुड़ी किसी भी सलाह को समझना बहुत ज़रूरी है. परफ़ॉर्मेंस के बारे में सलाह देने से लोगों को काफ़ी परेशानी होती है. कभी-कभी पहले अच्छी सलाह देने से, असल समस्याओं से लोगों का ध्यान भटक सकता है. आपको अपने वेब ऐप्लिकेशन की परफ़ॉर्मेंस का पूरा व्यू देखना होगा - परफ़ॉर्मेंस से जुड़ी इन सलाह पर ध्यान देने से पहले, आपको 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) में दो अलग-अलग जस्ट-इन-टाइम (जेआईटी) कंपाइलर होते हैं. असल में:

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

द फ़ुल कंपाइलर

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

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

इसलिए

  • पॉलीमॉर्फ़िक संक्रियाओं की तुलना में संक्रियाओं के एकल आकार के उपयोग को प्राथमिकता दी जाती है

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

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

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

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

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

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

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

d8 --trace-opt primes.js

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

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

इसलिए

  • अगर आपने {} अपवाद {} ब्लॉक को आज़माया है, तो नेस्ट किए गए फ़ंक्शन में परफ़ संवेदनशील कोड डालें: ```js function perf_sensitive() { // यहां पर प्रदर्शन के प्रति संवेदनशील काम करें } पर स्विच करने के मकसद से, हमसे संपर्क करने के लिए धन्यवाद.

आज़माएं { perf_sensitive() } catch (e) { // यहां अपवादों को मैनेज करें } पर स्विच करने के मकसद से, हमसे संपर्क करने के लिए धन्यवाद. ```

आने वाले समय में यह दिशा-निर्देश बदल सकता है, क्योंकि हम ऑप्टिमाइज़ करने वाले कंपाइलर में 'ट्राई/कैच ब्लॉक' चालू करते हैं. "--trace-opt" का इस्तेमाल करके, यह जांच किया जा सकता है कि कैसे ऑप्टिमाइज़ करने वाला कंपाइलर फ़ंक्शन को ब्लॉक कर रहा है d8 वाला विकल्प चुनें. इससे आपको इस बारे में ज़्यादा जानकारी मिलती है कि किन फ़ंक्शन का इस्तेमाल बंद कर दिया गया था:

d8 --trace-opt primes.js

ऑप्टिमाइज़ेशन बंद करना

आखिर में, इस कंपाइलर की ओर से किया गया ऑप्टिमाइज़ेशन अनुमान पर आधारित होता है - कभी-कभी यह काम नहीं करता है, और हम पीछे हट जाते हैं. "डीऑप्टिमाइज़ेशन" की प्रक्रिया ऑप्टिमाइज़ किए गए कोड को निकाल देता है और "full" में सही जगह पर एक्ज़ीक्यूशन को फिर से शुरू कर देता है कंपाइलर कोड. रीऑप्टिमाइज़ेशन, बाद में फिर से ट्रिगर हो सकता है. हालांकि, कुछ समय के लिए इसे लागू करने की रफ़्तार धीमी हो जाती है. खास तौर पर, फ़ंक्शन ऑप्टिमाइज़ होने के बाद वैरिएबल के छिपे हुए क्लास में बदलाव करने से, ऑप्टिमाइज़ेशन की यह प्रोसेस बंद हो जाएगी.

इसलिए

  • ऑप्टिमाइज़ होने के बाद, फ़ंक्शन में क्लास में किए गए छिपे हुए बदलावों से बचें

अन्य ऑप्टिमाइज़ेशन की तरह, उन फ़ंक्शन का लॉग देखा जा सकता है जिन्हें लॉग फ़्लैग की मदद से 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 चलाता है. हालांकि, अपने एल्गोरिदम को ऑप्टिमाइज़ करने पर भी ध्यान दें!

रेफ़रंस