यह लगातार तेज़ी से काम करता है
अपने पिछले लेखों में मैंने बताया था कि WebAssembly की मदद से, C/C++ के लाइब्रेरी नेटवर्क को वेब पर कैसे लाया जा सकता है. squoosh, हमारा वेब ऐप्लिकेशन है. यह C/C++ लाइब्रेरी का ज़्यादा से ज़्यादा इस्तेमाल करता है. इसकी मदद से, कई तरह के कोडेक का इस्तेमाल करके इमेज को कॉम्प्रेस किया जा सकता है. ये कोडेक, C++ से WebAssembly में कंपाइल किए गए होते हैं.
WebAssembly एक लो-लेवल वर्चुअल मशीन है, जो .wasm
फ़ाइलों में स्टोर किए गए बाइटकोड पर काम करती है. यह बाइट कोड, स्ट्रोंग टाइप और स्ट्रक्चर में इस तरह से होता है कि इसे होस्ट सिस्टम के लिए, JavaScript की तुलना में बहुत तेज़ी से कंपाइल और ऑप्टिमाइज़ किया जा सकता है. WebAssembly, ऐसे कोड को चलाने के लिए एक प्लैटफ़ॉर्म उपलब्ध कराता है जिसे शुरू से ही सैंडबॉक्सिंग और एम्बेड करने के लिए डिज़ाइन किया गया है.
मेरे अनुभव के मुताबिक, वेब पर परफ़ॉर्मेंस से जुड़ी ज़्यादातर समस्याएं, ज़बरदस्ती किए गए लेआउट और ज़्यादा पेंट की वजह से होती हैं. हालांकि, कभी-कभी किसी ऐप्लिकेशन को ऐसा काम करना पड़ता है जिसमें काफ़ी समय लगता है. WebAssembly यहां आपकी मदद कर सकता है.
द हॉट पाथ
हमने squoosh में एक JavaScript फ़ंक्शन लिखा है, जो इमेज बफ़र को 90 डिग्री के मल्टीपल में घुमाता है. OffscreenCanvas इसके लिए सबसे सही विकल्प होता, लेकिन यह उन सभी ब्राउज़र पर काम नहीं करता जिन्हें हमने टारगेट किया था. साथ ही, Chrome में यह थोड़ा गड़बड़ी वाला है.
यह फ़ंक्शन, इनपुट इमेज के हर पिक्सल को दोहराता है और उसे आउटपुट इमेज में किसी दूसरी जगह पर कॉपी करता है, ताकि उसे घुमाया जा सके. 4094 पिक्सल x 4096 पिक्सल (16 मेगापिक्सल) की इमेज के लिए, इनर कोड ब्लॉक को 1.6 करोड़ से ज़्यादा बार दोहराने की ज़रूरत होगी. इसे हम "हॉट पाथ" कहते हैं. बार-बार बार-बार इस्तेमाल करने के बावजूद, हमने तीन में से दो ब्राउज़र की जांच की है. इन ब्राउज़र ने टास्क को 2 सेकंड या उससे कम समय में पूरा कर लिया है. इस तरह के इंटरैक्शन के लिए स्वीकार की जाने वाली अवधि.
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;
}
}
हालांकि, एक ब्राउज़र में 8 सेकंड से ज़्यादा लगते हैं. ब्राउज़र जिस तरह JavaScript को ऑप्टिमाइज़ करते हैं वह काफ़ी मुश्किल है और अलग-अलग इंजन अलग-अलग चीज़ों के लिए ऑप्टिमाइज़ करते हैं. कुछ, रॉ एक्सीक्यूशन के लिए ऑप्टिमाइज़ होते हैं, कुछ डीओएम के साथ इंटरैक्शन के लिए ऑप्टिमाइज़ होते हैं. इस मामले में, हमें एक ब्राउज़र में ऐसा पाथ मिला है जिसे ऑप्टिमाइज़ नहीं किया गया है.
दूसरी ओर, WebAssembly को पूरी तरह से रॉ एक्सीक्यूशन स्पीड के हिसाब से बनाया गया है. इसलिए, अगर हमें इस तरह के कोड के लिए, सभी ब्राउज़र पर तेज़ और अनुमानित परफ़ॉर्मेंस चाहिए, तो WebAssembly की मदद ली जा सकती है.
बेहतर परफ़ॉर्मेंस के लिए WebAssembly
आम तौर पर, JavaScript और WebAssembly की परफ़ॉर्मेंस एक जैसी हो सकती है. हालांकि, JavaScript के लिए यह परफ़ॉर्मेंस सिर्फ़ "फास्ट पाथ" पर मिल सकती है. साथ ही, उस "फास्ट पाथ" पर बने रहना अक्सर मुश्किल होता है. WebAssembly का एक अहम फ़ायदा यह है कि यह सभी ब्राउज़र पर एक जैसी परफ़ॉर्म करता है. सख्त टाइपिंग और लो-लेवल आर्किटेक्चर की मदद से, कंपाइलर ज़्यादा बेहतर गारंटी दे सकता है. इससे, WebAssembly कोड को सिर्फ़ एक बार ऑप्टिमाइज़ करना पड़ता है और वह हमेशा “फास्ट पाथ” का इस्तेमाल करता है.
WebAssembly के लिए लिखना
पहले हमने C/C++ लाइब्रेरी को वेब पर उनकी सुविधाओं का इस्तेमाल करने के लिए, WebAssembly में कंपाइल किया था. हमने लाइब्रेरी के कोड में कोई बदलाव नहीं किया है. हमने ब्राउज़र और लाइब्रेरी के बीच ब्रिज बनाने के लिए, सिर्फ़ थोड़ा-बहुत C/C++ कोड लिखा है. इस बार हमारी वजह अलग है: हम WebAssembly को ध्यान में रखकर, कुछ नया लिखना चाहते हैं, ताकि हम WebAssembly के फ़ायदों का इस्तेमाल कर सकें.
WebAssembly का आर्किटेक्चर
WebAssembly के लिए कोड लिखते समय, यह समझना ज़रूरी है कि WebAssembly असल में क्या है.
WebAssembly.org को कोट करने के लिए:
C या Rust कोड के किसी हिस्से को WebAssembly में कंपाइल करने पर, आपको एक .wasm
फ़ाइल मिलती है. इसमें मॉड्यूल का एलान होता है. इस एलान में, "इंपोर्ट" की एक सूची दी गई है जिसे मॉड्यूल को अपने एनवायरमेंट से उम्मीद है. यह एक्सपोर्ट की एक ऐसी सूची है जिसे यह मॉड्यूल, होस्ट को उपलब्ध कराता है (फ़ंक्शन, कॉन्सटेंट, मेमोरी के हिस्से) और
इसमें मौजूद फ़ंक्शन के लिए असल बाइनरी निर्देश भी शामिल होते हैं.
मुझे इस बात का पता तब चला, जब मैंने इस बारे में पढ़ा: वह स्टैक जो WebAssembly को "स्टैक-आधारित वर्चुअल मशीन" बनाता है उसे उस मेमोरी के हिस्से में सेव नहीं किया जाता जिसका इस्तेमाल WebAssembly मॉड्यूल करते हैं. यह स्टैक पूरी तरह से VM के अंदर होता है और इसे वेब डेवलपर ऐक्सेस नहीं कर सकते. हालांकि, DevTools की मदद से इसे ऐक्सेस किया जा सकता है. इसलिए, ऐसे वेबअसेंबली मॉड्यूल लिखे जा सकते हैं जिन्हें किसी अतिरिक्त मेमोरी की ज़रूरत नहीं होती और वे सिर्फ़ VM के स्टैक का इस्तेमाल करते हैं.
हमारे मामले में, हमें अपनी इमेज के पिक्सल को मनमुताबिक ऐक्सेस करने और उस इमेज का घुमाया गया वर्शन जनरेट करने के लिए, कुछ अतिरिक्त मेमोरी का इस्तेमाल करना होगा. WebAssembly.Memory
का यही काम है.
मेमोरी मैनेज करना
आम तौर पर, अतिरिक्त मेमोरी का इस्तेमाल करने के बाद, आपको उस मेमोरी को मैनेज करना पड़ता है. मेमोरी के कौनसे हिस्से इस्तेमाल किए जा रहे हैं? कौनसे टूल बिना किसी शुल्क के उपलब्ध हैं?
उदाहरण के लिए, C में malloc(n)
फ़ंक्शन होता है, जो n
बाइट के लगातार मेमोरी स्पेस को ढूंढता है. इस तरह के फ़ंक्शन को "एलोकेटर" भी कहा जाता है.
बेशक, इस्तेमाल किए जा रहे ऐलोकेटर को लागू करने के तरीके को आपके WebAssembly मॉड्यूल में शामिल करना ज़रूरी है. इससे आपकी फ़ाइल का साइज़ बढ़ जाएगा. इन मेमोरी मैनेजमेंट फ़ंक्शन का साइज़ और परफ़ॉर्मेंस
इस्तेमाल किए जाने वाले एल्गोरिदम के हिसाब से काफ़ी अलग-अलग हो सकती है.
इसी वजह से, कई भाषाएँ चुनने के लिए कई सारे काम करने की सुविधा देती हैं
(जैसे, "dmalloc", "emmalloc", "wee_alloc" वगैरह).
हमारे मामले में, WebAssembly मॉड्यूल को चलाने से पहले, हमें इनपुट इमेज के डाइमेंशन (और इसलिए, आउटपुट इमेज के डाइमेंशन) के बारे में पता होता है. यहां हमें एक ऑपर्च्यूनिटी मिली: आम तौर पर, हम इनपुट इमेज के RGBA बफ़र को पैरामीटर के रूप में WebAssembly फ़ंक्शन में पास करते हैं और घुमाई गई इमेज को रिटर्न वैल्यू के रूप में दिखाते हैं. रिटर्न वैल्यू जनरेट करने के लिए, हमें एलोकेटर का इस्तेमाल करना होगा. हालांकि, हमें पता है कि कितनी मेमोरी की ज़रूरत है (इनपुट इमेज के साइज़ का दोगुना, एक बार इनपुट के लिए और एक बार आउटपुट के लिए). इसलिए, हम JavaScript का इस्तेमाल करके, इनपुट इमेज को WebAssembly मेमोरी में डाल सकते हैं. इसके बाद, दूसरी और घुमाई गई इमेज जनरेट करने के लिए, WebAssembly मॉड्यूल चलाएं और फिर नतीजे को पढ़ने के लिए JavaScript का इस्तेमाल करें. हम किसी भी मेमोरी मैनेजमेंट का इस्तेमाल किए बिना भी इससे रह सकते हैं!
कई विकल्पों में से चुनना
अगर आपने उस ओरिजनल JavaScript फ़ंक्शन को देखा है जिसे हमें WebAssembly में बदलना है, तो आपको पता चलेगा कि यह पूरी तरह से कैलकुलेशन वाला कोड है. इसमें JavaScript के लिए खास तौर पर बनाए गए एपीआई नहीं हैं. इसलिए, इस कोड को किसी भी भाषा में पोर्ट करना आसान होगा. हमने उन तीन अलग-अलग भाषाओं का आकलन किया है जो WebAssembly में कंपाइल होती हैं: C/C++, Rust, और AssemblyScript. हर भाषा के लिए, हमें सिर्फ़ एक सवाल का जवाब देना होगा: मेमोरी मैनेजमेंट फ़ंक्शन का इस्तेमाल किए बिना, हम रॉ मेमोरी को कैसे ऐक्सेस करते हैं?
C और Emscripten
Emscripten, WebAssembly टारगेट के लिए C कंपाइलर है. 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 में पोर्ट करने के बाद, emcc
की मदद से C फ़ाइल को कॉम्पाइल किया जा सकता है:
$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c
हमेशा की तरह, emscripten एक ग्लू कोड फ़ाइल जनरेट करता है, जिसे c.js
कहा जाता है. साथ ही, एक wasm मॉड्यूल जनरेट करता है, जिसे c.wasm
कहा जाता है. ध्यान दें कि Wasm मॉड्यूल का साइज़ सिर्फ़ ~260 बाइट है. हालांकि, gzip के बाद, ग्लू कोड का साइज़ 3.5 केबी के आस-पास है. कुछ समय तक कोशिश करने के बाद, हम ग्लू कोड को हटा पाए और वेनिला एपीआई के साथ WebAssembly मॉड्यूल को इंस्टैंशिएट कर पाए.
आम तौर पर, Emscripten के साथ ऐसा तब तक हो सकता है, जब तक कि C स्टैंडर्ड लाइब्रेरी में से कोई भी
इस्तेमाल नहीं किया जा रहा है.
Rust
Rust एक नई और आधुनिक प्रोग्रामिंग भाषा है. इसमें रिच टाइप सिस्टम है, रनटाइम नहीं है, और मालिकाना हक वाला मॉडल है. इससे मेमोरी और थ्रेड की सुरक्षा की गारंटी मिलती है. Rust, मुख्य सुविधा के तौर पर WebAssembly के साथ भी काम करता है. साथ ही, Rust टीम ने WebAssembly नेटवर्क में कई बेहतरीन टूल का योगदान दिया है.
इनमें से एक टूल wasm-pack
है. इसे rustvasm वर्किंग ग्रुप ने बनाया है. wasm-pack
आपका कोड लेकर उसे एक वेब-फ़्रेंडली मॉड्यूल में बदल देता है.
यह वेबपैक जैसे बंडलर के साथ
काम करता है. wasm-pack
का इस्तेमाल करना बेहद आसान है. फ़िलहाल, यह सिर्फ़ Rust के लिए उपलब्ध है. ग्रुप, वेब असेंबली को टारगेट करने वाली अन्य भाषाओं के लिए भी सहायता जोड़ने पर विचार कर रहा है.
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
इससे 7.6 केबी का wasm मॉड्यूल मिलता है, जिसमें करीब 100 बाइट का ग्लू कोड होता है. दोनों को gzip करने के बाद यह साइज़ मिलता है.
AssemblyScript
AssemblyScript एक नया प्रोजेक्ट है. इसका मकसद, TypeScript को 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 मॉड्यूल और कोई ग्लू कोड नहीं देगा. यह मॉड्यूल, सिर्फ़ वेबअसेंबली के सामान्य एपीआई के साथ काम करता है.
WebAssembly फ़ॉरेंसिक
Rust की 7.6 केबी की साइज़, दूसरी दो भाषाओं की तुलना में काफ़ी ज़्यादा है. WebAssembly नेटवर्क में कुछ टूल मौजूद हैं. इनकी मदद से, अपनी WebAssembly फ़ाइलों का विश्लेषण किया जा सकता है. भले ही, वे किसी भी भाषा में बनाई गई हों. साथ ही, इनसे आपको यह भी पता चलता है कि क्या हो रहा है और आपको अपनी स्थिति को बेहतर बनाने में भी मदद मिलती है.
ट्विगी
Twiggy, Rust की WebAssembly टीम का एक और टूल है. यह WebAssembly मॉड्यूल से अहम डेटा निकालता है. यह टूल सिर्फ़ Rust के लिए नहीं है. इसकी मदद से, मॉड्यूल के कॉल ग्राफ़ की जांच की जा सकती है. साथ ही, यह भी पता लगाया जा सकता है कि कौनसे सेक्शन इस्तेमाल नहीं किए गए हैं या कौनसे सेक्शन ज़रूरी नहीं हैं. इसके अलावा, यह भी पता लगाया जा सकता है कि आपके मॉड्यूल की फ़ाइल के कुल साइज़ में कौनसे सेक्शन का योगदान है. top
निर्देश का इस्तेमाल करके, Twiggy को यह निर्देश दिया जा सकता है:
$ twiggy top rotate_bg.wasm
इस मामले में, हम देख सकते हैं कि हमारी फ़ाइल का ज़्यादातर साइज़, ऐलोकेटर से आता है. यह आश्चर्य की बात थी, क्योंकि हमारा कोड डाइनैमिक ऐलोकेशन का इस्तेमाल नहीं कर रहा है. "फ़ंक्शन के नाम" सेक्शन भी इसकी एक बड़ी वजह है.
wasm-strip
wasm-strip
, WebAssembly Binary Toolkit या कम शब्दों में wabt का एक टूल है. इसमें कुछ ऐसे टूल शामिल हैं जिनकी मदद से, WebAssembly मॉड्यूल की जांच की जा सकती है और उनमें बदलाव किया जा सकता है.
wasm2wat
एक डिसअसेंबलर है, जो बाइनरी Wasm मॉड्यूल को ऐसे फ़ॉर्मैट में बदल देता है जिसे कोई भी व्यक्ति
आसानी से पढ़ सकता है. Wabt में wat2wasm
भी होता है. इसकी मदद से, आसानी से पढ़े जा सकने वाले फ़ॉर्मैट को वापस बाइनरी wasm मॉड्यूल में बदला जा सकता है. हमने अपनी वेब असेंबली फ़ाइलों की जांच करने के लिए, इन दोनों टूल का इस्तेमाल किया. हालांकि, हमें wasm-strip
सबसे ज़्यादा मददगार लगा. wasm-strip
, WebAssembly मॉड्यूल से ग़ैर-ज़रूरी सेक्शन और मेटाडेटा हटाता है:
$ wasm-strip rotate_bg.wasm
इससे, gzip के बाद, rust मॉड्यूल का फ़ाइल साइज़ 7.5 केबी से घटकर 6.6 केबी हो जाता है.
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]
कुछ सलाह और रिसर्च के बाद, हमने Rust की स्टैंडर्ड लाइब्रेरी का इस्तेमाल किए बिना, अपना Rust कोड फिर से लिखा. इसके लिए, हमने #![no_std]
सुविधा का इस्तेमाल किया. इससे, हमारे मॉड्यूल से एलोकेटर कोड हटाकर, डायनैमिक मेमोरी एलोकेशन की सुविधा भी पूरी तरह बंद हो जाती है. इस 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 की तरह ही, compiler भी कई चरणों में काम करता है. आसान शब्दों में कहें, तो पहला चरण कोड को इकट्ठा करने में ज़्यादा तेज़ होता है, लेकिन इससे धीमा कोड जनरेट होता है. मॉड्यूल के शुरू होने के बाद, ब्राउज़र यह देखता है कि कौनसे हिस्सों का अक्सर इस्तेमाल किया जाता है और उन्हें ज़्यादा ऑप्टिमाइज़ करने वाले, लेकिन धीमे कंपाइलर के ज़रिए भेजता है.
हमारा इस्तेमाल-उदाहरण दिलचस्प है, क्योंकि इमेज को घुमाने के लिए कोड का इस्तेमाल एक बार या दो बार किया जाएगा. इसलिए, ज़्यादातर मामलों में हमें ऑप्टिमाइज़ करने वाले कंपाइलर का फ़ायदा कभी नहीं मिलेगा. बेंचमार्क करते समय, इस बात का ध्यान रखना ज़रूरी है. हमारे WebAssembly मॉड्यूल को लूप में 10,000 बार चलाने से, अमान्य नतीजे मिलेंगे. सही आंकड़े पाने के लिए, हमें मॉड्यूल को एक बार चलाना चाहिए और उस एक रन की संख्याओं के आधार पर फ़ैसले लेने चाहिए.
परफ़ॉर्मेंस की तुलना
ये दोनों ग्राफ़, एक ही डेटा के अलग-अलग व्यू हैं. पहले ग्राफ़ में, हम हर ब्राउज़र की तुलना करते हैं. दूसरे ग्राफ़ में, हम इस्तेमाल की गई हर भाषा की तुलना करते हैं. कृपया ध्यान दें कि मैंने लॉगरिदमिक टाइमस्केल चुना है. यह भी ज़रूरी है कि सभी मानदंडों के लिए 16 मेगापिक्सल की जांच इमेज और एक ही होस्ट मशीन का इस्तेमाल किया जा रहा हो. हालांकि, एक ब्राउज़र का इस्तेमाल किया जा रहा था, जो एक ही मशीन पर नहीं चल सकता था.
इन ग्राफ़ का ज़्यादा विश्लेषण किए बिना भी यह साफ़ तौर पर पता चलता है कि हमने परफ़ॉर्मेंस से जुड़ी अपनी मूल समस्या को हल कर लिया है: सभी WebAssembly मॉड्यूल, ~500 मिलीसेकंड या उससे कम समय में काम करते हैं. इससे पता चलता है कि हमने शुरुआत में क्या बदलाव किया था: WebAssembly आपको अनुमानित परफ़ॉर्मेंस देता है. हम चाहे कोई भी भाषा चुनें, ब्राउज़र और भाषाओं के बीच का अंतर बहुत कम होता है. सटीक जानकारी के लिए: सभी ब्राउज़र में JavaScript के लिए स्टैंडर्ड डेविएशन ~400 मिलीसेकंड है, जबकि सभी ब्राउज़र में हमारे सभी WebAssembly मॉड्यूल के लिए स्टैंडर्ड डेविएशन ~80 मिलीसेकंड है.
प्रयास
दूसरी मेट्रिक यह है कि हमें अपने WebAssembly मॉड्यूल को स्क्वॉश में बनाने और इंटिग्रेट करने के लिए, मेहनत करनी पड़ी. मेहनत को संख्या में बदलना मुश्किल है. इसलिए, हम कोई ग्राफ़ नहीं बनाएंगे. हालांकि, हम कुछ बातों पर ध्यान दिलाना चाहते हैं:
AssemblyScript का इस्तेमाल करना आसान था. इसकी मदद से, WebAssembly लिखने के लिए TypeScript का इस्तेमाल किया जा सकता है. इससे मेरे साथ काम करने वाले लोगों के लिए, कोड की समीक्षा करना बहुत आसान हो जाता है. साथ ही, यह बिना किसी गड़बड़ी के WebAssembly मॉड्यूल बनाता है, जो बहुत छोटे होते हैं और बेहतर परफ़ॉर्म करते हैं. TypeScript नेटवर्क में मौजूद टूल, जैसे कि prettier और tslint, अब शायद काम करें.
wasm-pack
के साथ Rust का इस्तेमाल करना भी काफ़ी आसान है. हालांकि, यह उन बड़े वेब असेंबली प्रोजेक्ट के लिए ज़्यादा बेहतर है जिनमें बाइंडिंग और मेमोरी मैनेजमेंट की ज़रूरत होती है. हमें हैप्पी पाथ से कुछ अलग करना पड़ा,
ताकि हम कॉम्पटिटिव फ़ाइल साइज़ हासिल कर सकें.
C और Emscripten ने बिना किसी तैयारी के, बहुत छोटा और बेहतर परफ़ॉर्म करने वाला WebAssembly मॉड्यूल बनाया है. हालांकि, बिना ग्लू कोड में जाकर और उसे ज़रूरत के मुताबिक कम किए बिना, कुल साइज़ (WebAssembly मॉड्यूल + ग्लू कोड) काफ़ी बड़ा हो जाता है.
नतीजा
इसलिए, अगर आपके पास JS हॉट पाथ है और आपको इसे WebAssembly के साथ ज़्यादा तेज़ या बेहतर बनाना है, तो आपको किस भाषा का इस्तेमाल करना चाहिए. परफ़ॉर्मेंस से जुड़े सवालों के जवाब हमेशा की तरह ही हैं: यह इस बात पर निर्भर करता है. हमने क्या शिप किया?
हमने जिन अलग-अलग भाषाओं का इस्तेमाल किया है उनके मॉड्यूल साइज़ / परफ़ॉर्मेंस के उतार-चढ़ाव की तुलना में, सबसे अच्छा विकल्प C या AssemblyScript लग रहा है. हमने Rust को शिप करने का फ़ैसला किया है. यह फ़ैसला लेने की कई वजहें हैं: Squoosh में अब तक शिप किए गए सभी कोडेक को Emscripten का इस्तेमाल करके कंपाइल किया गया है. हमें WebAssembly नेटवर्क के बारे में ज़्यादा जानकारी चाहिए थी. साथ ही, प्रोडक्शन में किसी दूसरी भाषा का इस्तेमाल करना था. AssemblyScript एक अच्छा विकल्प है, लेकिन यह प्रोजेक्ट अपेक्षाकृत नया है और उसका कंपाइलर, Rust कंपाइलर के मुकाबले उतना बेहतर नहीं है.
हालांकि, Rust और दूसरी भाषाओं के साइज़ के बीच फ़ाइल के साइज़ में बहुत ज़्यादा अंतर दिखता है, लेकिन हकीकत में यह बहुत बड़ी बात नहीं है: 2G नेटवर्क पर भी 500B या 1.6 केबी लोड होने में 1/10 सेकंड से भी कम समय लगता है. साथ ही, हमें उम्मीद है कि Rust जल्द ही मॉड्यूल के साइज़ के मामले में, Python से आगे निकल जाएगा.
रनटाइम की परफ़ॉर्मेंस के मामले में, AssemblyScript के मुकाबले Rust की परफ़ॉर्मेंस सभी ब्राउज़र पर औसतन ज़्यादा तेज़ है. खास तौर पर, बड़े प्रोजेक्ट पर Rust तेज़ी से कोड जनरेट कर सकता है. इसके लिए, मैन्युअल कोड ऑप्टिमाइज़ेशन की ज़रूरत नहीं होती. हालांकि, इससे आपको अपने पसंदीदा टूल का इस्तेमाल करने से नहीं रोकना चाहिए.
इन सबके बावजूद: AssemblyScript एक बेहतरीन खोज है. इससे वेब डेवलपर, नई भाषा सीखे बिना WebAssembly मॉड्यूल बना सकते हैं. AssemblyScript की टीम बहुत ही जवाबदेह रही है और अपने टूलचेन को बेहतर बनाने के लिए लगातार काम कर रही है. हम आने वाले समय में, AssemblyScript पर ज़रूर नज़र बनाए रखेंगे.
अपडेट: Rust
इस लेख को पब्लिश करने के बाद, Rust टीम के Nick Fitzgerald ने हमें Rust Wasm की अपनी बेहतरीन किताब के बारे में बताया. इसमें फ़ाइल के साइज़ को ऑप्टिमाइज़ करने के बारे में एक सेक्शन है. वहां दिए गए निर्देशों का पालन करने पर, हम “सामान्य” Rust कोड लिख पाए. साथ ही, फ़ाइल के साइज़ को बढ़ाए बिना, Cargo
(Rust का npm
) का इस्तेमाल फिर से शुरू कर पाए. इन निर्देशों में, लिंक करने के समय ऑप्टिमाइज़ेशन और मैन्युअल रूप से पैनिक मैनेजमेंट की सुविधा चालू करना सबसे अहम था. gzip के बाद, Rust मॉड्यूल का साइज़ 370B हो जाता है. ज़्यादा जानकारी के लिए, कृपया Sqoosh पर खोला गया मेरा पीआर देखें.
इस सफ़र में मदद करने के लिए, ऐशली विलियम्स, स्टीव क्लैबनिक, निक फ़िट्ज़गेराल्ड, और मैक्स ग्रे को विशेष धन्यवाद.