यह लगातार तेज़ी से काम करता है
अपने पिछले लेखों में मैंने बताया था कि WebAssembly की मदद से, C/C++ के लाइब्रेरी नेटवर्क को वेब पर कैसे लाया जा सकता है. squoosh, हमारा वेब ऐप्लिकेशन है. यह C/C++ लाइब्रेरी का ज़्यादा से ज़्यादा इस्तेमाल करता है. इसकी मदद से, कई तरह के कोडेक का इस्तेमाल करके इमेज को कॉम्प्रेस किया जा सकता है. ये कोडेक, C++ से WebAssembly में कंपाइल किए गए होते हैं.
WebAssembly एक लो-लेवल वर्चुअल मशीन है, जो .wasm
फ़ाइलों में सेव किए गए बाइटकोड को चलाती है. इस बाइट कोड को स्ट्रोंग टाइप किया गया है और इसे इस तरह से व्यवस्थित किया गया है कि इसे होस्ट सिस्टम के लिए, JavaScript की तुलना में ज़्यादा तेज़ी से कंपाइल और ऑप्टिमाइज़ किया जा सकता है. WebAssembly, ऐसा कोड चलाने के लिए एक प्लैटफ़ॉर्म उपलब्ध कराता है जिसे शुरू से ही सैंडबॉक्सिंग और एम्बेड करने के लिए डिज़ाइन किया गया है.
मेरे अनुभव के मुताबिक, वेब पर परफ़ॉर्मेंस से जुड़ी ज़्यादातर समस्याएं, ज़बरदस्ती किए गए लेआउट और ज़्यादा पेंट की वजह से होती हैं. हालांकि, कभी-कभी किसी ऐप्लिकेशन को ऐसा काम करना पड़ता है जिसमें काफ़ी समय लगता है. WebAssembly से इसमें मदद मिल सकती है.
The Hot Path
हमने squoosh में एक JavaScript फ़ंक्शन लिखा है, जो इमेज बफ़र को 90 डिग्री के मल्टीपल में घुमाता है. OffscreenCanvas इसके लिए सबसे सही विकल्प होता, लेकिन यह उन सभी ब्राउज़र पर काम नहीं करता जिन्हें हमने टारगेट किया था. साथ ही, Chrome में यह थोड़ा गड़बड़ी वाला है.
यह फ़ंक्शन, इनपुट इमेज के हर पिक्सल को दोहराता है और उसे आउटपुट इमेज में किसी दूसरी जगह पर कॉपी करता है, ताकि उसे घुमाया जा सके. 4094 पिक्सल x 4096 पिक्सल (16 मेगापिक्सल) वाली इमेज के लिए, अंदरूनी कोड ब्लॉक को 16 मिलियन से ज़्यादा बार दोहराना होगा. इसे हम "हॉट पाथ" कहते हैं. इतने ज़्यादा दोहराव के बावजूद, हमने जिन तीन ब्राउज़र की जांच की उनमें से दो ब्राउज़र ने टास्क को दो सेकंड या उससे कम समय में पूरा कर लिया. इस तरह के इंटरैक्शन के लिए स्वीकार की जाने वाली अवधि.
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 मॉड्यूल को चलाने से पहले, हमें इनपुट इमेज के डाइमेंशन (और इसलिए, आउटपुट इमेज के डाइमेंशन) की जानकारी होती है. यहां हमें एक अवसर मिला: आम तौर पर, हम इनपुट इमेज के आरजीबीए बफ़र को पैरामीटर के तौर पर 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 मॉड्यूल, gzip करने के बाद सिर्फ़ ~260 बाइट का हो जाता है, जबकि ग्लू कोड, gzip करने के बाद करीब 3.5 केबी का हो जाता है. कुछ समय तक कोशिश करने के बाद, हम ग्लू कोड को हटा पाए और वेनिला एपीआई के साथ WebAssembly मॉड्यूल को इंस्टैंशिएट कर पाए.
Emscripten की मदद से, आम तौर पर ऐसा तब तक किया जा सकता है, जब तक C स्टैंडर्ड लाइब्रेरी का इस्तेमाल नहीं किया जा रहा है.
Rust
Rust एक नई और आधुनिक प्रोग्रामिंग भाषा है. इसमें रिच टाइप सिस्टम है, रनटाइम नहीं है, और मालिकाना हक वाला मॉडल है. इससे मेमोरी और थ्रेड की सुरक्षा की गारंटी मिलती है. Rust, मुख्य सुविधा के तौर पर WebAssembly के साथ भी काम करता है. साथ ही, Rust टीम ने WebAssembly नेटवर्क में कई बेहतरीन टूल का योगदान दिया है.
इनमें से एक टूल, rustwasm वर्किंग ग्रुप का wasm-pack
है. wasm-pack
आपके कोड को वेब के हिसाब से काम करने वाले मॉड्यूल में बदल देता है. यह मॉड्यूल, webpack जैसे बंडलर के साथ बिना किसी बदलाव के काम करता है. 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 की सुविधाओं को मॉडल करती है. इसका मतलब है कि आपके पास मौजूद किसी भी TypeScript को, WebAssembly में कॉम्पाइल नहीं किया जा सकता. हालांकि, इसका यह भी मतलब है कि आपको 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
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 की तरह ही, कमपाइलर कई चरणों में काम करता है. आसान शब्दों में कहें, तो पहला चरण कोड को इकट्ठा करने में ज़्यादा तेज़ होता है, लेकिन इससे धीमा कोड जनरेट होता है. मॉड्यूल के शुरू होने के बाद, ब्राउज़र यह देखता है कि कौनसे हिस्सों का अक्सर इस्तेमाल किया जाता है और उन्हें ज़्यादा ऑप्टिमाइज़ करने वाले, लेकिन धीमे कंपाइलर के ज़रिए भेजता है.
हमारा इस्तेमाल-उदाहरण दिलचस्प है, क्योंकि इमेज को घुमाने के लिए कोड का इस्तेमाल एक बार या दो बार किया जाएगा. इसलिए, ज़्यादातर मामलों में हमें ऑप्टिमाइज़ करने वाले कंपाइलर का फ़ायदा कभी नहीं मिलेगा. बेंचमार्क करते समय, इस बात का ध्यान रखना ज़रूरी है. हमारे WebAssembly मॉड्यूल को लूप में 10,000 बार चलाने से, अवास्तविक नतीजे मिलेंगे. सही आंकड़े पाने के लिए, हमें मॉड्यूल को एक बार चलाना चाहिए और उस एक बार के नतीजों के आधार पर फ़ैसले लेने चाहिए.
परफ़ॉर्मेंस की तुलना
ये दोनों ग्राफ़, एक ही डेटा के अलग-अलग व्यू हैं. पहले ग्राफ़ में, हम हर ब्राउज़र की तुलना करते हैं. दूसरे ग्राफ़ में, हम इस्तेमाल की गई हर भाषा की तुलना करते हैं. कृपया ध्यान दें कि मैंने लॉगरिदमिक टाइमस्केल चुना है. यह भी ज़रूरी है कि सभी बेंचमार्क में एक ही 16 मेगापिक्सल की टेस्ट इमेज और एक ही होस्ट मशीन का इस्तेमाल किया गया हो. हालांकि, एक ब्राउज़र को एक ही मशीन पर नहीं चलाया जा सका.
इन ग्राफ़ का ज़्यादा विश्लेषण किए बिना भी यह साफ़ तौर पर पता चलता है कि हमने परफ़ॉर्मेंस से जुड़ी अपनी मूल समस्या को हल कर लिया है: सभी WebAssembly मॉड्यूल, ~500 मिलीसेकंड या उससे कम समय में काम करते हैं. इससे इस बात की पुष्टि होती है कि हमने शुरुआत में जो कहा था वह सही है: WebAssembly की मदद से, आपको अनुमानित परफ़ॉर्मेंस मिलती है. हम चाहे कोई भी भाषा चुनें, ब्राउज़र और भाषाओं के बीच का अंतर बहुत कम होता है. सटीक जानकारी के लिए: सभी ब्राउज़र में JavaScript के लिए स्टैंडर्ड डेविएशन ~400 मिलीसेकंड है, जबकि सभी ब्राउज़र में हमारे सभी WebAssembly मॉड्यूल के लिए स्टैंडर्ड डेविएशन ~80 मिलीसेकंड है.
प्रयास
एक और मेट्रिक यह है कि हमें squoosh में अपना 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 और दूसरी भाषाओं के फ़ाइल साइज़ के बीच का फ़र्क़ काफ़ी ज़्यादा दिखता है. हालांकि, असल में यह फ़र्क़ इतना ज़्यादा नहीं है: 2 जीबी से ज़्यादा के लिए, 500B या 1.6 केबी को लोड करने में 10वें हिस्से से भी कम सेकंड लगते हैं. साथ ही, हमें उम्मीद है कि Rust जल्द ही मॉड्यूल के साइज़ के मामले में, Python से आगे निकल जाएगा.
रनटाइम की परफ़ॉर्मेंस के मामले में, AssemblyScript के मुकाबले Rust की परफ़ॉर्मेंस सभी ब्राउज़र पर औसतन ज़्यादा तेज़ है. खास तौर पर बड़े प्रोजेक्ट के लिए, Rust से तेज़ कोड जनरेट करने की संभावना ज़्यादा होती है. इसके लिए, कोड को मैन्युअल तरीके से ऑप्टिमाइज़ करने की ज़रूरत नहीं होती. हालांकि, इससे आपको अपने पसंदीदा टूल का इस्तेमाल करने से नहीं रोकना चाहिए.
इन सबके बावजूद: AssemblyScript एक बेहतरीन खोज है. इससे वेब डेवलपर, नई भाषा सीखे बिना WebAssembly मॉड्यूल बना सकते हैं. AssemblyScript की टीम बहुत ही जवाबदेह रही है और अपने टूलचेन को बेहतर बनाने के लिए लगातार काम कर रही है. हम आने वाले समय में, AssemblyScript पर ज़रूर नज़र बनाए रखेंगे.
अपडेट: Rust
इस लेख को पब्लिश करने के बाद, Rust टीम के Nick Fitzgerald ने हमें Rust Wasm की अपनी बेहतरीन किताब के बारे में बताया. इसमें फ़ाइल के साइज़ को ऑप्टिमाइज़ करने के बारे में एक सेक्शन है. वहां दिए गए निर्देशों का पालन करने पर, हम “सामान्य” Rust कोड लिख पाए. साथ ही, फ़ाइल के साइज़ को बढ़ाए बिना, Cargo
(Rust का npm
) का इस्तेमाल फिर से शुरू कर पाए. इन निर्देशों में, लिंक करने के समय ऑप्टिमाइज़ेशन और मैन्युअल रूप से पैनिक मैनेजमेंट की सुविधा चालू करना सबसे अहम था. gzip के बाद, Rust मॉड्यूल का साइज़ 370B हो जाता है. ज़्यादा जानकारी के लिए, कृपया Sqoosh पर खोला गया मेरा पीआर देखें.
इस प्रोसेस में मदद करने के लिए, ऐशली विलियम्स, स्टीव क्लैबनिक, निक फ़िट्जगेराल्ड, और मैक्स ग्रे का खास तौर पर धन्यवाद.