अपने ऐप्लिकेशन के JavaScript में किसी हॉट पाथ को WebAssembly से बदलना

यह लगातार तेज़ है, यो

मेरे पिछले लेख में मैंने बताया था कि WebAssembly की मदद से, C/C++ के लाइब्रेरी नेटवर्क को वेब पर कैसे लाया जा सकता है. C/C++ लाइब्रेरी का बहुत ज़्यादा इस्तेमाल करने वाला एक ऐप्लिकेशन squoosh है. हमारा ऐसा वेब ऐप्लिकेशन है जिसकी मदद से आप इमेज को कई तरह के कोडेक से कंप्रेस कर सकते हैं. इन कोड को C++ से WebAssembly तक इकट्ठा किया गया है.

WebAssembly एक लो-लेवल वर्चुअल मशीन है, जो .wasm फ़ाइलों में स्टोर किए गए बाइटकोड पर काम करती है. इस बाइट कोड को अच्छी तरह से टाइप और व्यवस्थित किया जाता है, ताकि इसे JavaScript की तुलना में तेज़ी से होस्ट सिस्टम के लिए कंपाइल और ऑप्टिमाइज़ किया जा सके. WebAssembly, ऐसे कोड को चलाने के लिए एनवायरमेंट उपलब्ध कराता है जिसमें शुरू से ही सैंडबॉक्सिंग और एम्बेडिंग की सुविधा होती थी.

मेरे हिसाब से, वेब पर परफ़ॉर्मेंस से जुड़ी ज़्यादातर समस्याएं, फ़ोर्स किए गए लेआउट और बहुत ज़्यादा पेंट की वजह से होती हैं. हालांकि, समय-समय पर ऐप्लिकेशन को कंप्यूटर का एक महंगा काम करना पड़ता है, जिसमें बहुत ज़्यादा समय लगता है. WebAssembly यहां आपकी मदद कर सकता है.

द हॉट पाथ

स्क्वॉश में हमने एक JavaScript फ़ंक्शन लिखा था, जो इमेज बफ़र को 90 डिग्री के मल्टीपल में रोटेट करता है. हालांकि, इसके लिए OffscreenCanvas सबसे सही रहेगा, लेकिन यह उन ब्राउज़र पर काम नहीं करता जिन्हें हम टारगेट कर रहे थे. साथ ही, यह Chrome में कुछ गड़बड़ी के साथ भी काम नहीं करता.

यह फ़ंक्शन किसी इनपुट इमेज के हर पिक्सल के ऊपर फिर से काम करता है और रोटेशन पाने के लिए, आउटपुट इमेज में इसे किसी अलग जगह पर कॉपी करता है. 4094 पिक्सल x 4096 पिक्सल (16 मेगापिक्सल) की इमेज के लिए, इनर कोड ब्लॉक को 1.6 करोड़ बार दोहराने की ज़रूरत होगी. इसे हम "हॉट पाथ" कहते हैं. बार-बार बार-बार इस्तेमाल करने के बावजूद, हमने तीन में से दो ब्राउज़र की जांच की है. इन ब्राउज़र ने टास्क को दो सेकंड या उससे कम समय में पूरा कर लिया है. इस तरह के इंटरैक्शन के लिए स्वीकार की जाने वाली अवधि.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

हालांकि, एक ब्राउज़र को आठ सेकंड से ज़्यादा समय लगता है. ब्राउज़र जिस तरह JavaScript को ऑप्टिमाइज़ करते हैं वह काफ़ी मुश्किल है और अलग-अलग इंजन अलग-अलग चीज़ों के लिए ऑप्टिमाइज़ करते हैं. कुछ कॉन्फ़िगरेशन, प्रोसेस नहीं किए जा सकते और कुछ को डीओएम के साथ इंटरैक्शन के लिए ऑप्टिमाइज़ किया जाता है. इस मामले में, हमने एक ब्राउज़र में ऐसे पाथ पर क्लिक किया है जो ऑप्टिमाइज़ नहीं किया गया है.

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

परफ़ॉर्मेंस का अनुमान लगाने के लिए WebAssembly

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

WebAssembly के लिए लिखना

इससे पहले हम C/C++ लाइब्रेरी को लेते थे और उन्हें WebAssembly में इकट्ठा करते थे, ताकि वे वेब पर उनके काम करने के तरीके का इस्तेमाल कर सकें. हमने लाइब्रेरी के कोड को ठीक से नहीं लिखा, ब्राउज़र और लाइब्रेरी को जोड़ने के लिए थोड़ा-बहुत C/C++ कोड लिखा. इस बार हमारा मकसद अलग है: हम WebAssembly को ध्यान में रखते हुए, शुरुआत से कुछ लिखना चाहते हैं, ताकि WebAssembly के फ़ायदों का इस्तेमाल किया जा सके.

WebAssembly आर्किटेक्चर

WebAssembly के लिए लिखते समय, इस बारे में थोड़ा और जानना बेहतर होता है कि WebAssembly क्या है.

WebAssembly.org को कोट करने के लिए:

जब WebAssembly में C या Rust कोड का कोई हिस्सा इकट्ठा किया जाता है, तो आपको एक .wasm फ़ाइल मिलती है. इस फ़ाइल में, एक मॉड्यूल की जानकारी होती है. इस एलान में, "इंपोर्ट" की एक सूची दी गई है जिसे मॉड्यूल को अपने एनवायरमेंट से उम्मीद है. यह एक्सपोर्ट की एक ऐसी सूची है जिसे यह मॉड्यूल, होस्ट को उपलब्ध कराता है (फ़ंक्शन, कॉन्सटेंट, मेमोरी के हिस्से) और इसमें मौजूद फ़ंक्शन के लिए असल बाइनरी निर्देश भी शामिल होते हैं.

जब तक मैंने इसे खोजा, तब तक मुझे यह समझ नहीं आया: WebAssembly को "स्टैक-आधारित वर्चुअल मशीन" बनाने वाला स्टैक, WebAssembly मॉड्यूल की मेमोरी में इस्तेमाल की जाने वाली मेमोरी में सेव नहीं होता. स्टैक पूरी तरह से वर्चुअल मशीन (वीएम) के लिए इंटरनल है और इसे वेब डेवलपर ऐक्सेस नहीं कर सकते. हालांकि, DevTools की मदद से ही इसे ऐक्सेस किया जा सकता है. ऐसा करने से, ऐसे WebAssembly मॉड्यूल लिखे जा सकते हैं जिन्हें ज़्यादा मेमोरी की ज़रूरत नहीं होती. साथ ही, इनमें सिर्फ़ वीएम-इंटरनल स्टैक का इस्तेमाल किया जाता है.

हमारे मामले में, हमें अपनी इमेज के पिक्सल का आर्बिट्रेरी ऐक्सेस देने के लिए कुछ और मेमोरी का इस्तेमाल करना होगा और उस इमेज का घुमाया गया वर्शन जनरेट करना होगा. यह WebAssembly.Memory के लिए है.

मेमोरी मैनेज करना

आम तौर पर, एक बार ज़्यादा मेमोरी इस्तेमाल करने के बाद, आपको किसी न किसी तरह से उस मेमोरी को मैनेज करने की ज़रूरत पड़ सकती है. मेमोरी के कौनसे हिस्से इस्तेमाल किए जा रहे हैं? कौनसी सेवाएं बिना किसी शुल्क के उपलब्ध हैं? उदाहरण के लिए, C में आपके पास malloc(n) फ़ंक्शन है, जो लगातार n बाइट का मेमोरी स्पेस ढूंढता है. इस तरह के फ़ंक्शन को "एलोकेटर" भी कहा जाता है. बेशक, इस्तेमाल किए जा रहे ऐलोकेटर को लागू करने के तरीके को आपके WebAssembly मॉड्यूल में शामिल करना ज़रूरी है. इससे आपकी फ़ाइल का साइज़ बढ़ जाएगा. इन मेमोरी मैनेजमेंट फ़ंक्शन का साइज़ और परफ़ॉर्मेंस इस्तेमाल किए जाने वाले एल्गोरिदम के हिसाब से काफ़ी अलग-अलग हो सकती है. इसी वजह से, कई भाषाएँ चुनने के लिए कई सारे काम करने की सुविधा देती हैं ("dmalloc", "emmalloc", "wee_alloc" वगैरह).

हमारे मामले में, हम WebAssembly मॉड्यूल को चलाने से पहले, इनपुट इमेज के डाइमेंशन (और इसलिए आउटपुट इमेज के डाइमेंशन) को जानते हैं. यहां हमें एक ऑपर्च्यूनिटी मिली: पारंपरिक तौर पर, हम इनपुट इमेज के RGBA बफ़र को पैरामीटर के रूप में WebAssembly फ़ंक्शन में पास करेंगे और घुमाई गई इमेज को रिटर्न वैल्यू के रूप में दिखाएंगे. इस रिटर्न वैल्यू को जनरेट करने के लिए, हमें ऐलोकेटर का इस्तेमाल करना होगा. हालांकि, हमें पता है कि ज़रूरी मेमोरी (इनपुट के लिए दो बार, एक बार इनपुट और आउटपुट के लिए एक बार) मेमोरी की ज़रूरत है. इसलिए, हम JavaScript का इस्तेमाल करके इनपुट इमेज को WebAssembly मेमोरी में डाल सकते हैं. दूसरी, घुमाई गई इमेज जनरेट करने के लिए WebAssembly मॉड्यूल चलाएं और फिर नतीजा पढ़ने के लिए JavaScript का इस्तेमाल करें. हम किसी भी मेमोरी मैनेजमेंट का इस्तेमाल किए बिना भी इससे रह सकते हैं!

पसंद के हिसाब से बनाया गया

अगर आपने ओरिजनल JavaScript फ़ंक्शन पर नज़र डाली है, जिसे हम WebAssembly-fy देखना चाहते हैं, तो आपको पता चलेगा कि यह पूरी तरह से कंप्यूटेशनल कोड है. इसमें किसी खास JavaScript वाले एपीआई का इस्तेमाल नहीं किया गया है. इसलिए, ज़रूरी है कि इस कोड को किसी भी भाषा में पोर्ट किया जा सके. हमने WebAssembly में इकट्ठा की गई तीन अलग-अलग भाषाओं का आकलन किया: C/C++, Rust, और AssemblyScript. हर भाषा के लिए हमें सिर्फ़ एक ही सवाल का जवाब देना होगा: हम मेमोरी मैनेजमेंट फ़ंक्शन का इस्तेमाल किए बिना, रॉ मेमोरी कैसे ऐक्सेस करते हैं?

C और एमस्क्रिप्टन

Emscripten, WebAssembly टारगेट के लिए सी कंपाइलर है. Emscripten का लक्ष्य GCC या clang जैसे जाने-माने C कंपाइलर की जगह पर आने वाला काम करना है और वह ज़्यादातर फ़्लैग के साथ काम करता है. यह Emscripten के मिशन का एक अहम हिस्सा है, क्योंकि यह मौजूदा C और C++ कोड को WebAssembly में कंपाइल करना चाहता है.

रॉ मेमोरी को ऐक्सेस करना, C का काम करता है. इसी वजह से, पॉइंटर भी मौजूद होते हैं:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

यहां हम 0x124 संख्या को पॉइंटर में बदलकर, साइन नहीं किए गए 8-बिट वाले पूर्णांक (या बाइट) में बदल रहे हैं. इससे ptr वैरिएबल, मेमोरी पते 0x124 से शुरू होने वाले अरे में बदल जाता है, जिसका इस्तेमाल हम दूसरे अरे की तरह कर सकते हैं. इससे हमें पढ़ने और लिखने के लिए अलग-अलग बाइट का ऐक्सेस मिलता है. हमारे मामले में, हम उस इमेज का आरजीबीए बफ़र देख रहे हैं जिसे हम रोटेशन पाने के लिए फिर से क्रम में लगाना चाहते हैं. एक पिक्सल को मूव करने के लिए, हमें एक बार में लगातार चार बाइट मूव करनी होंगी (हर चैनल के लिए एक बाइट: R, G, B, और A). इसे आसान बनाने के लिए, हम बिना साइन वाले 32-बिट वाले पूर्णांकों की कलेक्शन बना सकते हैं. कन्वेंशन के मुताबिक, हमारी इनपुट इमेज पते 4 से शुरू होगी और हमारी आउटपुट इमेज, इनपुट इमेज के खत्म होने के बाद सीधे शुरू हो जाएगी:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

पूरे JavaScript फ़ंक्शन को C में पोर्ट करने के बाद, हम C फ़ाइल को emcc के साथ कंपाइल कर सकते हैं:

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

हमेशा की तरह, emscripten एक ग्लू कोड फ़ाइल जनरेट करता है, जिसे c.js कहते हैं. साथ ही, emscripten, c.wasm नाम का एक Wasm मॉड्यूल जनरेट करता है. ध्यान दें कि Wasm मॉड्यूल का साइज़ सिर्फ़ ~260 बाइट है. हालांकि, gzip के बाद, ग्लू कोड का साइज़ 3.5 केबी के आस-पास है. कुछ कदम आगे बढ़ने के बाद, हम ग्लू कोड को भूल गए और वनिला एपीआई की मदद से WebAssembly मॉड्यूल को इंस्टैंशिएट कर पाए. आम तौर पर, Emscripten के साथ ऐसा तब तक हो सकता है, जब तक कि C स्टैंडर्ड लाइब्रेरी में से कोई भी इस्तेमाल नहीं किया जा रहा है.

Rust

Rust, एक नई और मॉडर्न प्रोग्रामिंग भाषा है. इसमें रिच टाइप वाला सिस्टम है. इसमें रनटाइम नहीं है. यह मालिकाना हक वाला मॉडल है, जो मेमोरी और थ्रेड की सुरक्षा की गारंटी देता है. Rust, WebAssembly की मुख्य सुविधा के तौर पर भी काम करता है. साथ ही, Rust की टीम ने WebAssembly नेटवर्क के लिए काफ़ी बेहतरीन टूल उपलब्ध कराया है.

इनमें से एक टूल wasm-pack है. इसे rustvasm वर्किंग ग्रुप ने बनाया है. wasm-pack आपका कोड लेकर उसे एक वेब-फ़्रेंडली मॉड्यूल में बदल देता है. यह वेबपैक जैसे बंडलर के साथ काम करता है. wasm-pack एक बहुत ही सुविधाजनक अनुभव है, लेकिन फ़िलहाल यह सिर्फ़ Rust के लिए काम करता है. यह ग्रुप, WebAssembly में टारगेट की जाने वाली अन्य भाषाओं के लिए सहायता जोड़ने पर विचार कर रहा है.

Rust में, C वाली सरणियों को स्लाइस होता है. और ठीक C की तरह ही, हमें ऐसे स्लाइस बनाने होंगे जो हमारे शुरुआती पतों का इस्तेमाल करते हैं. यह उस मेमोरी सुरक्षा मॉडल के ख़िलाफ़ है जिसे Rust ने लागू किया है. इसलिए, हमें unsafe कीवर्ड का इस्तेमाल करना पड़ता है, ताकि हम ऐसा कोड लिख सकें जो उस मॉडल के मुताबिक नहीं है.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

इसका इस्तेमाल करके Rust फ़ाइलों को कंपाइल करना

$ wasm-pack build

करीब 100 बाइट के ग्लू कोड (gzip के बाद) के साथ 7.6 केबी का Wasm मॉड्यूल होता है.

AssemblyScript

AssemblyScript एक छोटा प्रोजेक्ट है, जो TypeScript-to-WebAssembly कंपाइलर के तौर पर काम करता है. हालांकि, यह ध्यान रखना ज़रूरी है कि यह सिर्फ़ किसी TypeScript का इस्तेमाल नहीं करेगा. AssemblyScript उसी सिंटैक्स का इस्तेमाल करती है जो TypeScript का इस्तेमाल करती है. हालांकि, यह स्टैंडर्ड लाइब्रेरी को खुद बदल देती है. उनकी स्टैंडर्ड लाइब्रेरी, WebAssembly की क्षमताओं का मॉडल बनाती है. इसका मतलब है कि WebAssembly के आस-पास मौजूद किसी भी TypeScript को कंपाइल नहीं किया जा सकता. इसका यह मतलब है कि आपको WebAssembly लिखने के लिए नई प्रोग्रामिंग भाषा सीखने की ज़रूरत नहीं है!

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

हमारे rotate() फ़ंक्शन में छोटे टाइप की सतह को ध्यान में रखते हुए, इस कोड को AssemblyScript में पोर्ट करना काफ़ी आसान था. रॉ मेमोरी को ऐक्सेस करने के लिए, AssemblyScript की ओर से load<T>(ptr: usize) और store<T>(ptr: usize, value: T) फ़ंक्शन दिए गए हैं. हमारी AssemblyScript फ़ाइल को कंपाइल करने के लिए, हमें सिर्फ़ AssemblyScript/assemblyscript npm पैकेज इंस्टॉल करना होगा और उसे चलाना होगा

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript हमें ~300 बाइट Wasm मॉड्यूल और नहीं ग्लू कोड देगा. यह मॉड्यूल बस vanilla WebAssembly API के साथ काम करता है.

WebAssembly Forensics

अन्य भाषाओं की तुलना में, Rust का साइज़ 7.6 केबी से ज़्यादा है. WebAssembly नेटवर्क में कुछ ऐसे टूल हैं जो आपकी WebAssembly फ़ाइलों का विश्लेषण करने में मदद कर सकते हैं. भले ही, इन फ़ाइलों को किसी भी भाषा में बनाया गया हो. इनसे आपको समस्याओं के बारे में जानकारी और स्थिति को बेहतर बनाने में मदद मिलती है.

ट्विगी

Twiggy, Rust की WebAssembly टीम का एक और टूल है, जो WebAssembly मॉड्यूल से अहम जानकारी वाला कई डेटा निकालता है. यह टूल, रस्ट पर आधारित नहीं है. इसकी मदद से, मॉड्यूल के कॉल ग्राफ़ जैसी चीज़ों की जांच की जा सकती है. साथ ही, इस्तेमाल न किए गए या ग़ैर-ज़रूरी सेक्शन का पता लगाया जा सकता है. साथ ही, यह पता लगाया जा सकता है कि आपके मॉड्यूल की फ़ाइल के कुल साइज़ पर कौनसे सेक्शन की वजह से असर पड़ रहा है. बाद में, Twiggy के top निर्देश का इस्तेमाल किया जा सकता है:

$ twiggy top rotate_bg.wasm
Twiggy इंस्टॉल करने का स्क्रीनशॉट

इस मामले में हम देख सकते हैं कि हमारे ज़्यादातर फ़ाइल साइज़ एलोकेटर से आते हैं. यह आश्चर्य की बात है, क्योंकि हमारा कोड डायनैमिक एलोकेशन का इस्तेमाल नहीं कर रहा है. "फ़ंक्शन के नाम" सब-सेक्शन, भी एक बड़ा योगदान है.

Wasm-Strip

wasm-strip, WebAssembly बाइनरी टूलकिट का एक टूल है. इसका इस्तेमाल छोटे शब्दों में किया जाता है. इसमें कुछ ऐसे टूल हैं जिनकी मदद से WebAssembly मॉड्यूल की जांच और उनमें बदलाव किया जा सकता है. wasm2wat एक डिसअसेंबलर है, जो बाइनरी Wasm मॉड्यूल को ऐसे फ़ॉर्मैट में बदल देता है जिसे कोई भी व्यक्ति आसानी से पढ़ सकता है. Wabt में wat2wasm भी होता है, जिसकी मदद से लोग उस फ़ॉर्मैट को फिर से बाइनरी Wasm मॉड्यूल में बदल सकते हैं, जिसे आसानी से पढ़ा जा सकता है. हालांकि, हमने अपनी WebAssembly फ़ाइलों की जांच करने के लिए, इन दो मददगार टूल का इस्तेमाल किया है. इसके बावजूद, हमें पता चला है कि wasm-strip सबसे काम का है. wasm-strip, WebAssembly मॉड्यूल से गै़र-ज़रूरी सेक्शन और मेटाडेटा हटा देता है:

$ wasm-strip rotate_bg.wasm

इससे रस्ट मॉड्यूल की फ़ाइल का साइज़ 7.5 केबी से कम होकर 6.6 केबी (gzip के बाद) हो जाएगा.

wasm-opt

wasm-opt, Binaryen का एक टूल है. इसके लिए, एक WebAssembly मॉड्यूल ज़रूरी होता है. साथ ही, यह सिर्फ़ बाइट कोड के आधार पर, साइज़ और परफ़ॉर्मेंस, दोनों के लिए इसे ऑप्टिमाइज़ करने की कोशिश करता है. Emscripten जैसे कुछ टूल इस टूल को पहले से ही चलाते हैं, लेकिन कुछ दूसरे नहीं. आम तौर पर इन टूल का इस्तेमाल करके कुछ अतिरिक्त बाइट आज़माना और सेव करना अच्छा होता है.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

wasm-opt के साथ हम एक और बाइट शेव कर सकते हैं और gzip के बाद कुल 6.2 केबी रख सकते हैं.

#![no_std]

कुछ सलाह और रिसर्च के बाद, हमने #![no_std] सुविधा का इस्तेमाल करके, Rust की स्टैंडर्ड लाइब्रेरी का इस्तेमाल किए बिना Rust कोड को फिर से लिखा. इससे डाइनैमिक मेमोरी ऐलोकेशन को भी बंद कर दिया जाता है और हमारे मॉड्यूल से एलोकेटर कोड हट जाता है. इस Rust फ़ाइल को इनके साथ कंपाइल किया जा रहा है

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

wasm-opt, wasm-strip, और gzip के बाद, 1.6 केबी का Wasm मॉड्यूल मिला. हालांकि, यह C और AssemblyScript के जनरेट किए गए मॉड्यूल से अब भी बड़ा है, लेकिन लाइटवेट माने जाने के लिए यह काफ़ी छोटा है.

परफ़ॉर्मेंस

सिर्फ़ फ़ाइल के साइज़ के आधार पर किसी नतीजे पर पहुंचने से पहले, हम फ़ाइल के साइज़ के बजाय परफ़ॉर्मेंस को ऑप्टिमाइज़ करने के लिए गए. हमने परफ़ॉर्मेंस को कैसे मापा और नतीजे कैसे मिले?

मानदंड कैसे बनाएं

WebAssembly एक लो-लेवल बाइट कोड फ़ॉर्मैट होने के बावजूद, इसे होस्ट के लिए खास मशीन कोड जनरेट करने के लिए, कंपाइलर की मदद से भेजना पड़ता है. JavaScript की तरह ही, कंपाइलर कई स्टेज में काम करता है. आसान शब्दों में कहें: पहला चरण कंपाइल करने में ज़्यादा तेज़ होता है, लेकिन आम तौर पर ज़्यादा धीमा कोड जनरेट करता है. मॉड्यूल शुरू होने के बाद, ब्राउज़र यह देखता है कि कौनसे पार्ट अक्सर इस्तेमाल किए जाते हैं. इसके बाद, वह उन हिस्सों को ज़्यादा ऑप्टिमाइज़ होने वाले, लेकिन धीमे कंपाइलर की मदद से भेजता है.

हमारे उपयोग का उदाहरण यह दिलचस्प है कि किसी इमेज को घुमाने के लिए कोड का उपयोग एक बार या फिर दो बार किया जाएगा. इसलिए ज़्यादातर मामलों में, हमें ऑप्टिमाइज़ करने वाले कंपाइलर के फ़ायदे कभी नहीं मिलेंगे. मानदंडों का इस्तेमाल करते समय इस बात को ध्यान में रखना ज़रूरी है. हमारे WebAssembly मॉड्यूल को लूप में 10,000 बार चलाने से, गैर-भरोसेमंद नतीजे मिलेंगे. असली आंकड़े पाने के लिए, हमें मॉड्यूल को एक बार चलाना चाहिए और उस एक रन की संख्याओं के आधार पर फ़ैसले लेने चाहिए.

परफ़ॉर्मेंस की तुलना

हर भाषा के हिसाब से स्पीड की तुलना
प्रति ब्राउज़र गति की तुलना

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

इन ग्राफ़ का बहुत ज़्यादा विश्लेषण किए बिना, यह साफ़ है कि हमने अपनी मूल परफ़ॉर्मेंस की समस्या को हल कर लिया है: सभी WebAssembly मॉड्यूल ~500 मि॰से॰ या इससे कम समय में चलते हैं. इससे पता चलता है कि हमने शुरुआत में क्या बदलाव किया था: WebAssembly आपको अनुमानित परफ़ॉर्मेंस देता है. हम चाहे कोई भी भाषा चुनें, ब्राउज़र और भाषाओं के बीच बहुत कम अंतर होता है. सटीक जानकारी: सभी ब्राउज़र के लिए JavaScript का स्टैंडर्ड डीविएशन ~400 मि॰से॰ है, जबकि हमारे सभी WebAssembly मॉड्यूल का सभी ब्राउज़र के लिए स्टैंडर्ड डीविएशन ~80 मि॰से॰ है.

प्रयास

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

AssemblyScript बिना किसी रुकावट के था. यह आपको WebAssembly लिखने के लिए TypeScript का इस्तेमाल करने की अनुमति भी देता है. इससे मेरे साथ काम करने वालों के लिए, कोड की समीक्षा करना काफ़ी आसान हो जाता है. साथ ही, यह ग्लू-फ़्री WebAssembly मॉड्यूल भी बनाता है, जो बहुत छोटे होते हैं और उनकी परफ़ॉर्मेंस अच्छी होती है. TypeScript नेटवर्क में मौजूद टूल, जैसे कि prettier और tslint, अब शायद काम करें.

wasm-pack के साथ रस्ट का इस्तेमाल करना भी बहुत आसान है. हालांकि, WebAssembly के बड़े प्रोजेक्ट में बेहतर तरीके से काम करना, दोनों के लिए ज़रूरी था और मेमोरी को मैनेज करना भी ज़रूरी था. हमें हैप्पी पाथ से कुछ अलग करना पड़ा, ताकि हम कॉम्पटिटिव फ़ाइल साइज़ हासिल कर सकें.

C और Emscripten ने बेहद छोटा और बेहतर परफ़ॉर्म करने वाला WebAssembly मॉड्यूल बनाया है. हालांकि, इसमें ग्लू कोड में जाकर इसे कम करना और कुल साइज़ (WebAssembly मॉड्यूल और ग्लू कोड) की ज़रूरत नहीं पड़ती. इतना ही नहीं, यह काफ़ी बड़ा हो जाता है.

नतीजा

अगर आपके पास JS हॉट पाथ है और आपको उसे WebAssembly के साथ तेज़ या एक जैसा बनाना है, तो आपको किस भाषा का इस्तेमाल करना चाहिए. हमेशा की तरह, परफ़ॉर्मेंस से जुड़े सवालों का जवाब है: यह कई चीज़ों पर निर्भर करता है. हमने क्या भेजा?

तुलना करने वाला ग्राफ़

हमने जिन अलग-अलग भाषाओं का इस्तेमाल किया है उनके मॉड्यूल साइज़ / परफ़ॉर्मेंस के उतार-चढ़ाव की तुलना में, सबसे अच्छा विकल्प C या AssemblyScript लग रहा है. हमने Rust को भेजने का फ़ैसला किया. यह फ़ैसला लेने की कई वजहें हैं: Squoosh में अब तक शिप किए गए सभी कोडेक को Emscripten का इस्तेमाल करके कंपाइल किया गया है. हम WebAssembly नेटवर्क के बारे में अपनी जानकारी बढ़ाना चाहते थे. साथ ही, प्रोडक्शन में एक अलग भाषा का इस्तेमाल करना चाहते थे. AssemblyScript एक अच्छा विकल्प है, लेकिन प्रोजेक्ट थोड़ा छोटा है. साथ ही, कंपाइलर Rust कंपाइलर जितना मैच्योर नहीं है.

हालांकि, Rust और दूसरी भाषाओं के साइज़ के बीच फ़ाइल के साइज़ का अंतर स्कैटर ग्राफ़ में काफ़ी बड़ा दिखता है, लेकिन असल में यह बहुत बड़ी बात नहीं है: 500B या 1.6 केबी लोड होने में, 2G के लिए भी 1/10 सेकंड से भी कम समय लगता है. उम्मीद है कि रस्ट जल्द ही मॉड्यूल के साइज़ के मामले में इस अंतर को कम कर देगा.

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

बस इतना ही कहा जा रहा है: AssemblyScript एक बेहतरीन खोज है. इसकी मदद से, वेब डेवलपर नई भाषा सीखे बिना WebAssembly मॉड्यूल बना सकते हैं. AssemblyScript की टीम बहुत मददगार रही है और अपने टूलचेन को बेहतर बनाने पर लगातार काम कर रही है. आने वाले समय में, हम AssemblyScript पर नज़र रखेंगे.

अपडेट: Rust

इस लेख को पब्लिश करने के बाद, Rust की टीम के सदस्य निक फ़िट्ज़गेरल्ड ने Rust Wasm की शानदार किताब के बारे में बताया. इस किताब में, फ़ाइल के साइज़ को ऑप्टिमाइज़ करने के बारे में एक सेक्शन है. वहां दिए गए निर्देशों का पालन करने (सबसे खास तौर पर, लिंक टाइम ऑप्टिमाइज़ेशन और मैन्युअल पैनिक हैंडलिंग को चालू करने की सुविधा) की वजह से, हमें रस्ट कोड को "सामान्य" लिखने और फ़ाइल साइज़ को बड़ा किए बिना Cargo (रस्ट का npm) का इस्तेमाल करने में मदद मिली. gzip के बाद Rust मॉड्यूल के अंत में 370B मिलता है. ज़्यादा जानकारी के लिए, कृपया Squoosh पर मैंने जो पीआर खोला उसे देखें.

इस सफ़र में मदद करने के लिए, ऐशली विलियम्स, स्टीव क्लाबनिक, निक फ़िट्ज़गेराल्ड, और मैक्स ग्रे को विशेष धन्यवाद.