SIMD एल्गोरिद्म को शुरुआत से डिज़ाइन करना
(mcyoung.xyz)- Rust के
std::simdसे बने vb64 base64 codec में procedural loop को जस का तस vectorize करने के बजाय, data layout और operation flow को circuit की तरह फिर से design करना पड़ता है, तभी तेज़ और portable SIMD code बनता है - मुख्य optimization branching और memory access से होने वाले stall को घटाने में है; compare, mask, select और shuffle से ऐसी branchless structure बनाई जाती है जो input से स्वतंत्र होकर वही operations करती है
- base64 decoding में ASCII characters को sextet में बदलने के लिए
byte >> 4और/correction का इस्तेमाल करते हुए perfect hash बनाया जाता है, और SIMD vector के अंदर lookup table व shuffle से offset खोजा जाता है - चार 6-bit sextet को तीन bytes में pack करते समय lane को
u16तक बढ़ाकर shift किया जाता है, फिर low/high byte अलग किए जाते हैं औरrotate_lanes_leftव OR से adjacent lane के byte fragments जोड़े जाते हैं - benchmark में
-Zbuild-std,-Ctarget-cpu=native,N = 32combination और remainder loading optimization के बाद crates.io की baseline base64 implementation की तुलना में लगभग पूरे range में करीब 2x performance दिखी
SIMD की ज़रूरत का भौतिक background
- computer performance में सुधार सिर्फ theoretical CS से नहीं, बल्कि physical constraints से सीधे जुड़ा है
- Moore’s law 2023 तक अब भी कायम दिखता है, लेकिन पिछले 15 सालों में Dennard scaling का असर टूट गया, जिससे अधिक dense transistors power consumption density बढ़ाने लगे
- clock frequency को लगातार बढ़ाना मुश्किल होने के बाद, 2000s की शुरुआत से performance improvement की मुख्य दिशा ज़्यादा cores इस्तेमाल करने की ओर शिफ्ट हुई
- multithreading में cores के बीच cooperation चाहिए, जिससे synchronization cost आती है; jump, virtual call और synchronization जैसे control flow stall पैदा करते हैं
- stall के दो मुख्य कारण हैं
- branching:
if, loops, function calls, function returns, C काswitchजैसे control flow - memory operations: load/store, खासकर cache-friendly न होने वाले access
- branching:
Procedural code और instruction-level parallelism
- modern CPU core code को line-by-line execute नहीं करता, बल्कि एक-दूसरे पर निर्भर न होने वाले operations को साथ-साथ issue करता है
a = x + yऔरb = x ^ yजैसे operations जो एक-दूसरे पर निर्भर नहीं हैं, add और xor circuits को साथ-साथ इस्तेमाल कर सकते हैं- यही तरीका instruction-level parallelism है, और इसे बाधित करने वाली dependencies को data hazard कहा जाता है
- CPU जितना बेहतर functional units को saturate कर पाता है, उतने अधिक operations प्रति unit time process करता है
- branch में अगला instruction fetch करने से पहले condition calculation का इंतज़ार करना पड़ता है, और memory operation में data को physically CPU तक पहुंचना होता है, इसलिए stall होता है
- GPU images को vector-form pixels की तरह handle करता है और high locality वाले operations बहुत करता है, इसलिए वह batch operations और limited control flow के लिए design की गई SIMD machine के काफ़ी करीब है
- SIMD का मतलब single instruction, multiple data है, यानी एक instruction कई data lanes पर parallel operation करता है
lane-level सोच
- SIMD और vector अक्सर एक ही अर्थ में इस्तेमाल होते हैं, और SIMD instruction की basic unit fixed-size number array यानी vector होती है
- vector के हर component को lane कहा जाता है
- SIMD vector को register में fit होना होता है, इसलिए वह आम तौर पर छोटा होता है
- example environment की maximum vector width 256 bits है
- यह
u8x32के 32 bytes याf64x8के 4 doubles के बराबर है
- छोटा vector भी अगर pipeline saturation का burden 4x घटा सके, तो उतना ही latency improvement मिल सकता है
popcnt से दिखता divide-and-conquer
- सबसे simple vector operations bitwise and/or/xor हैं
- normal integer को भी bitwise operation के नज़रिए से 1-bit lanes के vector की तरह देखा जा सकता है
- इस नज़रिए से
i32,i1x32जैसा है
- इस नज़रिए से
popcntinteger के अंदर 1 bits की संख्या गिनने वाला operation है, औरi32कोi1x32मानें तो यह reduce operation है- 32 bits को array की तरह निकालकर जोड़ने वाली simple implementation खराब code बना सकती है
- बेहतर तरीका है adjacent bit pairs को जोड़ना, फिर pairs के pairs को जोड़ना, यानी lane width बढ़ाते हुए sum करना
0x55555555,0xaaaaaaaamasks से even/odd bits अलग करना- shift से lanes align करके जोड़ना
- फिर 2-bit, 4-bit, 8-bit, 16-bit units में दोहराना
- यह implementation
popcntinstruction में optimize नहीं होती, लेकिन ऐसे systems पर छोटा और तेज़ code बनती है जहाँ वह instruction नहीं है u64पर भी reduction का सिर्फ एक और step जोड़कर इसे लागू किया जा सकता है, और पूरेu64addition की ज़रूरत नहीं होती- ऐसा divide-and-conquer approach SIMD programming का core pattern है
SIMD instruction set के मुख्य tools
- real SIMD vectors scalar से ज़्यादा complex semantics देते हैं, और slow control flow को replace करने वाली capabilities खास तौर पर महत्वपूर्ण हैं
- available instructions architecture पर काफी निर्भर करते हैं
- x86 के कई high-performance cores AVX2 implement करते हैं
- AVX2 256-bit
ymmvectors देता है - registers में खुद lane count नहीं होता; instruction तय करता है कि lanes को कैसे interpret करना है
- उदाहरण के लिए
vpaddb,ymmकोi8x32की तरह interpret करता है
- आम तौर पर उपलब्ध operations ये हैं
- bitwise operations: lane width हमेशा implicitly 1 bit होती है
- lane-wise arithmetic: addition, subtraction, multiplication, division, integer shift, min/max आदि
- lane-wise comparison:
m[i] = a[i] < b[i]जैसा mask vector बनाता है - select: mask का उपयोग करके दो vectors में से lane-wise value चुनता है
- shuffle/swizzle: एक vector को lookup table की तरह देखकर index vector से lanes को rearrange करता है
- mask vector के true/false आम तौर पर all-ones या all-zeros bit pattern इस्तेमाल करते हैं
- comparison और select ऐसे core tools हैं जो SIMD code को branchless बनाए रखने में मदद करते हैं
- branchless code input से स्वतंत्र होकर वही operations करता है, और
x * 0 = 0,a ^ b ^ a = bजैसे properties से unnecessary results को discard करता है
shuffle से data position align करना
- SIMD में data को “सही position” पर लाने के लिए shuffle core tool है
- broadcast या splat ऐसा vector बनाता है जिसमें सभी lanes के पास वही scalar होता है, और इसे
[0, 0, ...]index shuffle से express किया जा सकता है - interleave या zip/pack दो vectors
a,bकी lanes को बारी-बारी arrange करता हैc = [a[0], b[0], a[1], b[1], ...]- इसे shuffle2 से implement किया जा सकता है
- deinterleave या unzip/unpack, interleave का उल्टा है
- rotate
b[i] = a[(i + j) % n]रूप में lanes को rotate करता है, और यह भी shuffle ही है - SIMD programming में integer से बड़े data blocks को अलग-अलग sizes के छोटे blocks के रूप में reinterpret और rearrange करना अक्सर होता है
intrinsics, target feature, portable SIMD
- SIMD में उपयोग किए जा सकने वाले operations architecture और instruction set extension के अनुसार बदलते हैं
- x86 में ऐसे operations हो सकते हैं जो ARM में नहीं हैं, और Intel AVX-512 की तरह एक ही vendor के भीतर भी ऐसे extensions हो सकते हैं जो केवल advanced server chips में मिलते हैं
- toolchain ऐसे extensions को target feature के रूप में generalize करता है
- Linux का
lscpuCPU द्वारा पहचाने जाने वाले features दिखाता है - LLVM feature settings के अनुसार instruction selection अलग तरह से करता है
- LLVM को
ymmइस्तेमाल करने वाला code emit करने के लिए+avx2होना ज़रूरी है
- Linux का
-march=nativeया-Ctarget-cpu=nativebuild करने वाली machine के हिसाब से अच्छा code बना सकते हैं, लेकिन दूसरे processors पर portability घट सकती है- runtime feature detection वह तरीका है जिसमें CPU द्वारा support की जाने वाली capabilities की जाँच कर तय किया जाता है कि function का कौन-सा version call करना है; यह encryption libraries जैसे उन codebases में इस्तेमाल होता है जिन्हें कई तरह के devices पर deploy किया जाता है
- C++ का SIMD code आम तौर पर
_mm256_cvtps_epu32जैसे intrinsics का उपयोग करता है- ये किसी specific instruction set के low-level operations को दर्शाते हैं
- ये ज़रूरी नहीं कि हमेशा एक single instruction में map हों
- compiler merging, duplicate removal और instruction selection optimization कर सकता है
- अगर कई instruction sets के लिए मिलता-जुलता code बार-बार लिखना पड़े, तो assembly की तुलना में maintainability का लाभ बहुत बड़ा नहीं रह सकता
- portable SIMD library का approach यह है कि library level पर कुछ instruction selection handle किया जाए और बाकी compiler पर छोड़ दिया जाए
vb64implementation यह जाँचने का experiment है कि Rust का portable SIMD competitive code generate करता है या नहीं
base64 decoding को SIMD में बदलना
- base64 arbitrary binary data को ASCII में encode करने का तरीका है
- input byte sequence को bit vector मानकर उसे 6-bit chunks, यानी sextet, में बाँटा जाता है
- sextet value को निम्न characters में map किया जाता है
0..25→'A'..'Z'26..51→'a'..'z'52..61→'0'..'9'62→+63→/
- base64 के कई variants हैं, लेकिन complexity का ज़्यादातर हिस्सा common है
- ध्यान देने वाली दो बातें हैं
- base64 ऐसा format है जिसमें byte के भीतर bits big endian होते हैं
- input length 4 से divide न भी हो सकता है; सिद्धांततः इसे
=padding से 4 के multiple में मिलाया जाता है, लेकिन गलत padding वाले messages भी handle किए जा सकते हैं
- decoded length की गणना
input / 4 * 3मेंinput % 4के अनुसार बची हुई length जोड़कर की जाती है
branchless की ओर basic refactoring
- simple base64 decoder में कई branches होते हैं
- input को chunks में iterate करने वाला loop
- chunk के भीतर byte loop
- ASCII characters के अनुसार
match - error होने पर
return Err decoded_lenके भीतरmatchVec::extend_from_sliceऔर allocator call की संभावना
- optimization guideline है सभी branches हटाना
decoded_lenकाmatch,input % 4की values0, 1, 2, 3को0, 1, 1, 2में map करता है- इसे
mod4 - mod4 / 2से बदलने पर branchless version बन जाता है - LLVM मूल
matchको switch table में fold कर सकता है, लेकिन इस area में अनावश्यक memory access performance घटाता है
सबसे hot loop को अलग करना
- SIMD की ताकत यह है कि वह एक बार में बहुत सारा data process करके loop को strongly unroll कर सकता है और उसे branchless के करीब बना सकता है
- hot loop का लक्ष्य अधिकतम 4 bytes पढ़ना, अधिकतम 3 bytes का decoded result बनाना, और syntax error है या नहीं यह भी बताना है
- तीन facts का उपयोग किया जा सकता है
- output length branchless
decoded_len()से calculate की जा सकती है - invalid base64 को बहुत rare path माना जा सकता है, और error position चाहिए तो बाद में फिर से scan किया जा सकता है
- base64 में
Aका मान 0 होता है, इसलिए truncated chunk कोAसे pad करने पर value नहीं बदलती
- output length branchless
decode_hot()को इस रूप में अलग किया जाता है कि वह चार input bytes process करे और decoded result तथा success status का bool return करेOption<[u8; 3]>के बजाय bool अलग से return करने पर आगेif !okbranch हटाना आसान होता है- SIMD version में input के रूप में
Simd<u8, 4>लिया जाता है, और output को भी power-of-two lane count के अनुरूपSimd<u8, 4>रखा जाता है- वास्तव में आवश्यक output 3 bytes है
- आखिरी lane इस्तेमाल नहीं होता
ASCII को sextet में बदलने का तरीका
- ASCII character को sextet में बदलने वाले
matchका ज़्यादातर हिस्साbyte - Cके रूप में व्यक्त किया जा सकता है'A'..'Z'→byte - 'A''a'..'z'→byte - 'a' + 26'0'..'9'→byte - '0' + 52'+'→byte - '+' + 62'/'→byte - '/' + 63
- lane-wise offset vector बनाकर
ascii - offsetsकिया जा सकता है - पहला approach compare-and-select है
A-Z,a-z,0-9,+,/के लिए masks बनाए जाते हैं- जिस lane में कोई भी mask select नहीं हुआ, उसे invalid माना जाता है
- हर mask से संबंधित offset को splat कर OR से combine किया जाता है
- यह तरीका elegant और competitive code बना सकता है, लेकिन कुल 8 comparisons चाहिए और कई live values रहने से register pressure बन सकता है
SIMD hash table और perfect hash
A-Z,a-z,0-9की byte ranges क्रमशः0x41..0x5b,0x61..0x7b,0x30..0x3aहैं और उनका high nibble अलग है+और/क्रमशः0x2b,0x2fहैं, इसलिए सिर्फbyte >> 4से ज़्यादातर distinction किया जा सकता है/के case में एक घटाने पर range के लिए perfect hash बन जाता है(byte >> 4) - (byte == '/')की mapping इस प्रकार हैA-Z→ 4 या 5a-z→ 6 या 70-9→ 3+→ 2/→ 1
- यह value छोटी है, इसलिए offset lookup table को SIMD vector के अंदर रखकर shuffle से lookup किया जा सकता है
- इस perfect hash idea को GitHub issue में एक anonymous user ने सुझाया था
Simd::swizzle_dyn()में यह constraint है कि index array और lookup table की length समान होनी चाहिए- perfect hash method में sextet calculation के दौरान validation side effect के रूप में नहीं मिलती, इसलिए उसी GitHub issue के exact bloom filter का उपयोग करके byte validity check की जाती है
- implementation example vb64 के simd.rs में है
चार sextets को तीन bytes में pack करना
- चार 6-bit sextets को तीन bytes में combine करने वाला step अधिक tricky है
- किसी specific input sextet को all-ones रखकर और output में bits कहाँ move होते हैं यह देखकर placement relationship trace किया जा सकता है
- केवल byte-level shuffle पर्याप्त नहीं है
- जिस target पर move करना है वह byte fragment है
- सिर्फ shift भी पर्याप्त नहीं है
- overshifted bits को adjacent lane में move करना होता है
- समाधान है lane को बड़ा बनाना
sextetsकोu16vector में cast करने के बाद lane-wise shift किया जाता हैinput[0]को 2-bit shiftinput[1]को 4-bit shiftinput[2]को 6-bit shiftinput[3]को 8-bit shift से adjust किया जाता है
- shift result से low byte और high byte vectors अलग किए जाते हैं
hi.rotate_lanes_left::<1>()से high byte side के fragments को adjacent lane के साथ align करने के बादlo | hi_rotatedसे combine किया जाता है- यह तरीका hardware primitives का actively उपयोग करता है, इसलिए code छोटा और efficient है
lane की संख्या बढ़ाना और garbage lane हटाना
Simd<u8, 4>x86 के न्यूनतम 128-bit vector register से भी छोटा है, इसलिएdecode_hot()को lane की संख्याNके लिए generic बनाया गयाLaneCount<N>: SupportedLaneCountconstraint से छोटी power-of-two lane संख्या सुनिश्चित होती है- lookup table और shift table
tiled()helper के जरिए repeating pattern वाले vector बनाते हैं N = 4में आखिरी lane की garbage value को ignore करना काफी था, लेकिनNबड़ा होने पर every fourth lane में garbage मिल जाती है- इसे हटाने के लिए shuffle का उपयोग किया गया
- वांछित संबंध है
shuffled[i] = output[i + i / 3] - हर चौथे index को skip करके garbage lane हटाई जाती है
- overflow वाला हिस्सा final output vector का ऊपरी 1/4 होता है, इसलिए उसे ignore किया जाता है
- वांछित संबंध है
- इससे
decode_hot::<32>()के जरिए 32 base64 byte को parallel decode किया जा सकता है
outer loop optimization
decode()को भी अंदरूनी lane संख्याNके लिए generic बनाया गया- बची हुई लागतें इस प्रकार हैं
for chunks in ...की length comparison branch[T]::copy_from_sliceका variable-lengthmemcpy- हर loop iteration में
okbranch Vec::extend_from_sliceकी allocator call की संभावना और एक औरmemcpy
- output length पता होने के कारण
out.reserve(final_len + N / 4)से पहले ही space reserve किया गया - इसके अलावा slop space रखकर variable-length memcpy के बजाय full SIMD store किया गया
- हर iteration पूरा SIMD vector लिखता है, और अगला write
3/4 * Nजितना आगे बढ़कर पिछली garbage byte को overwrite करता है - आखिरी garbage byte final
Vec::set_len()में शामिल नहीं होती, इसलिए उसे हटाई गई मानकर handle किया जाता है if !okके कारण early return होने पर भी,set_len()से commit नहीं किया गया होता, इसलिएoutunchanged state में रहता है
error handling को hot loop के बाहर टालना
- हर iteration में
if !okसे return करने के बजाय,error |= !okसे accumulate किया गया - final
set_len()से ठीक पहले केवल एक बार error status check किया गया - यह मानते हुए कि ज्यादातर base64 blob valid होते हैं, error path को hot loop से बाहर धकेल दिया गया
- syntax error होने पर भी बाद के SIMD operations मनमाने ढंग से misbehave नहीं करते, इसलिए garbage write commit नहीं होता और गायब हो जाता है
- इसके बाद
Vec::push()जैसी calls उसी buffer region को overwrite कर सकती हैं
unroll and jam और remainder handling
copy_from_sliceके variable-length memcpy को घटाने के लिए unroll and jam लागू किया गया- loop को दो हिस्सों में बांटा गया
- hot vectorized loop: हमेशा length
Ninput ही process करता है - cold remainder part:
i < Ninput को अधिकतम एक बार process करता है
- hot vectorized loop: हमेशा length
- Rust के
Iterator::chunks_exact()का उपयोग करके hand-rolled unroll-and-jam implement किया गया - hot loop में
Simd::from_slice()call करके single vector-sized load किया जाता है - bounds check ऐसा रूप ले लेता है जिसे compiler के लिए हटाना आसान होता है
benchmark और manual loading optimization
- benchmark में length 0 से लगभग 200 या 500 byte तक के messages decode किए गए और crates.io के baseline base64 implementation से compare किया गया
- compile options में
-Zbuild-stdऔर-Ctarget-cpu=nativeका उपयोग किया गया - tuning के परिणामस्वरूप
N = 32सबसे अच्छा निकला, और हर hot loop iteration में एक YMM register का उपयोग होता है - शुरुआत में baseline से बेहतर प्रदर्शन मिला, लेकिन
data.len() % 32से strongly correlated heartbeat जैसी performance variation दिखी - assembly देखने के बाद अनुमान लगाया गया कि
copy_from_slicebyte-wise load loop की तरह inline/unroll हुआ है Simd::gather_or()भी आजमाया गया, लेकिन उसने और खराब assembly बनाई, इसलिए उसका उपयोग नहीं किया गया- इसके बजाय variable-length data के लिए manual loading function लिखा गया
- hot part में loop के अंदर संभव सबसे बड़ा scalar load, यानी
u128load, किया गया - LLVM 16-byte chunk को XMM load में lower करता है
- remainder के लिए overlapping
u64,u32,u8load का उपयोग किया गया
- hot part में loop के अंदर संभव सबसे बड़ा scalar load, यानी
- 15 byte पढ़ते समय
pसेu64, औरp + 7सेu64पढ़कर 1 byte overlap कराया गया और OR से combine किया गया - 4~7 byte के लिए overlapping
u32load इस्तेमाल किया गया - 1~3 byte के लिए
p,p + len/2,p + len - 1से पढ़कर कुछ bytes duplicate load हो सकते हैं, लेकिन branch count घटता है - नया loading code लागू करने के बाद variance बहुत कम हो गया, और baseline की तुलना में लगभग पूरे range में 2x performance दिखी
encoding और web-safe base64
- encoding function के लिए
decode_hot()के operations को उल्टा करने वालाencode_hot()implement करना होता है - decoding में इस्तेमाल किया गया perfect hash encoding के लिए fit नहीं बैठता, इसलिए नया hash चाहिए
- encoder के आसपास का loading/storing code भी decoder से थोड़ा अलग है
vb64efficient encoding routine भी implement करता है- web-safe base64 एक variant है जिसमें
+और/को-और_से बदला जाता है - web-safe base64 के perfect hash की संरचना अधिक कठिन है, और उदाहरण के तौर पर
(byte >> 4) - (byte == '_' ? '_' : 0)जैसी method की जरूरत पड़ सकती है vb64अभी web-safe base64 support नहीं करता
निष्कर्ष
vb64कोई ऐसी library नहीं है जो किसी महत्वपूर्ण bottleneck को solve करने का दावा करती हो, और लेखक बताते हैं कि उन्हें ऐसी जगह नहीं पता जहां base64 decoding सच में bottleneck हो- branchless code अक्सर overkill होता है, लेकिन यह समझने में मदद करता है कि compiler क्या कर सकता है और क्या नहीं
- Rust का
std::simdकुल मिलाकर अच्छा है और बेहतरीन code generate करता है - SIMD code को और सरल बनाने के लिए कुछ rough edges हैं जिन्हें ठीक किया जाना अच्छा होगा, लेकिन वर्तमान काम के परिणाम से संतुष्टि जताई गई
- SIMD और performance optimization जटिल विषय हैं जिनमें बहुत-सी tricks और hardware knowledge चाहिए, और उनमें से काफी कुछ documented नहीं है
1 टिप्पणियां
Hacker News की राय
portable SIMD को सच में इस्तेमाल होते देखना रोचक था, और Zen 3 सिस्टम पर benchmark दोहराकर देखा तो वही speedup मिला
M1 MacBook Pro पर input length 110 bytes होने पर performance gain 1.4x से शुरू होकर धीरे-धीरे 2x तक पहुँचा; x86_64 से कम है, लेकिन लगता है लक्ष्य हासिल हो गया
हालांकि code देखकर मेरे इस अनुभव की पुष्टि होती है कि Rust में SIMD और pointer से जुड़े कामों, और व्यापक रूप से performance engineering के लिए ergonomics काफ़ी खराब हैं
फिर भी Rust का portable SIMD, C++ की तुलना में अभी कोई बहुत अच्छी कहानी नहीं है, और raw byte regions, pointers, buffers manipulation तक उतरना हो तो
Pin,MaybeUninitवगैरह से परिचित होना पड़ता हैportable_simdऔरallocator_apiकई सालों से unstable हैं, entry barrier भी ऊँचा है और वे ज़्यादा awkward हैं, मगर इसका अधिकतर हिस्सा intentional design हैहालांकि अपने program के भीतर इस्तेमाल आसान करने वाले abstractions खुद बनाने या third-party crates इस्तेमाल करने से कोई नहीं रोकता
C++ SSE intrinsic में underscores भी भद्दे लगते हैं और नाम याद रखना भी मुश्किल है, वह कहीं ज़्यादा खराब है
classic C++ में अपनी तरफ़ से best implementation करने के बाद, जब कोई SIMD version बनाकर उसे 10 गुना से भी ज़्यादा तेज़ कर देता है, तो कभी-कभी सचमुच हैरानी होती है
बदले में यह code कम portable होता है
काश compiler की auto-vectorization और बेहतर हो, और language level पर कुछ annotations जैसा support भी हो जो local स्तर पर कुछ operations reordering की अनुमति दे
compiler बहुत local context से बाहर data को आपकी तरफ़ से बदल नहीं सकता, इसलिए auto-vectorization सच में कठिन हो जाती है
उदाहरण के लिए
for(double v: vec) sum+=vमें floating-point addition associative नहीं होता, इसलिए values को क्रम से जोड़ना और SIMD तरीके से 8-8 के gap में जोड़कर बाकी को merge करना समान नहीं हैcompiler के नज़रिए से यह obvious optimization लग सकता है, लेकिन जब तक आप उसे किसी खास guarantee को relax करने के लिए न बताएं, वह optimization से पहले serial semantics guarantee को प्राथमिकता देता है
इसलिए चीज़ें गड़बड़ हो जाती हैं, और janwas के कहे अनुसार hot paths के लिए libraries, खासकर Google Highway या Intel ISPC जैसी चीज़ें इस्तेमाल करना बेहतर लगता है
जितना हो सके portable तरीके से efficient रहने की कोशिश करते हुए भी, ज़रूरत पड़ने पर target-specific programming आसान बनाना
auto-vectorization में FORTRAN compilers निश्चित तौर पर बेहतर हैं, क्योंकि aliasing allowed नहीं होती
C++ C के memory model को follow करने के कारण अटक जाता है
CUDA आज की ultimate SIMD machine, यानी GPU, के लिए design किया गया C++ है, और ROCm असल में AMD के लिए CUDA जैसा ही है
निजी तौर पर मुझे Microsoft का C++AMP पसंद था; मुझे लगता है वह शुरू करने के लिए सबसे आसान था
लेकिन आखिरकार वह जम नहीं पाया, यह अफ़सोस की बात है
साथ ही SIMD wrapper library इस्तेमाल करने पर असल में इसे काफ़ी portable बनाया जा सकता है
छोटी-सी note के तौर पर, compiler उस
popcountimplementation को single instruction में optimize नहीं कर पाया, लेकिन दूसरी implementation में यह संभव हैबेशक, यह काफ़ी tricky है: https://godbolt.org/z/T69KxWWW8
_mm256_cvtps_epu32के बारे में कहा गया कि यह किसी specific instruction set का low-level operation दर्शाता है और इसे AVX2 का float-to-int cast बताया गया, लेकिन वह instruction AVX-512 में आती हैAVX2 में float-to-int cast नहीं है, और AVX1 में integer result signed होता है तथा instruction
_mm256_cvtps_epi32हैfastbase64[0] से तुलना कैसी होगी, यह जानने की उत्सुकता है
लेख बढ़िया है और ऐसी चीज़ें online देखना अच्छा लगता है, लेकिन portable SIMD library को लेकर लेखक जितने optimistic हैं, उतना मैं साझा नहीं कर पाता
[0]: https://github.com/lemire/fastbase64
SIMD को C++ या Rust में जोड़ने से बेहतर मुझे ISPC ही लगता है
यह dynamic dispatch भी support करता है, जिसे खुद implement करना दर्दनाक feature है
ताकि C++ में वापस inline calls भेज सकें, SIMD code में templates और classes इस्तेमाल कर सकें, और कई SIMD code regions को साथ में inline भी कर सकें
dynamic dispatch implement करना मुश्किल है, इससे सहमत हूँ, लेकिन Highway वह हिस्सा संभाल देता है
शानदार लेख है, और यह एहसास बहुत ज़ोर से रह जाता है कि “मैं कभी इतना smart नहीं हो पाऊँगा”
कुछ वैसा ही जैसे आम लोग software engineer या physicist नहीं होते
कुछ महीनों तक focus करके पढ़ें तो आप similar level तक कर पाएँगे
आखिरकार यह interest और need का मामला है
मैं भी personal projects में performance optimization या systems के करीब bare-metal engineering में आता-जाता रहता हूँ, लेकिन काश काम में इसकी ज़रूरत ज़्यादा पड़ती
हालांकि industry के ज़्यादातर कामों में इसकी demand नहीं होती
idiomatic Python नहीं, बल्कि सब कुछ numpy से हल करने जैसा
यह मज़ेदार है और इस तरह की cleverness सीखी जा सकती है; लेख के कई हिस्से उन भाषाओं में समस्याएँ हल करने की सोच के हिसाब से बहुत natural लगते हैं
समय के साथ आप समस्याओं को उसी रूप में देखने लगते हैं
दिलचस्प लेख है
शुरुआत के पहले उदाहरण में कहा गया है कि non-vectorized
popcntimplementation “ईमानदारी से कहें तो हास्यास्पद रूप से खराब code” बनाता है, लेकिन release mode में native target CPU इस्तेमाल करने पर वह function काफी ठीक-ठाक vectorize होता दिखता हैhttps://godbolt.org/z/WE1Eq65jY
pub fn popcnt(mut x: u32) -> u32 { x.count_ones() }यह
popcnt eax, edi; retमें compile होता हैबड़े bit vectors में AVX2 implementation
POPCNTसे तेज़ हो सकता है“Faster Population Counts Using AVX2 Instructions” देखें: https://academic.oup.com/comjnl/article/61/1/111/3852071
32-bit पर्याप्त बड़ा नहीं है, और Rust जो code generate करता है वह सच में हास्यास्पद रूप से खराब है
popcntinstruction में lower होना चाहिएहाल ही में मैंने ऐसा code लिखा था जिसमें vector operation के result mask में bits की संख्या count करनी थी, और यह अच्छी तरह
popcntमें बदल गयाhttps://godbolt.org/z/zT9Whcnco
“यह तो trap question जैसा है… बस add नहीं है क्या?” जैसे हिस्सों की वजह से आम तौर पर मन करता है कि intermediate vector representation को target किया जाए और details compiler पर छोड़ दी जाएँ
उदाहरण के लिए Haswell chips में प्रति core कई floating-point execution units थे, और CPU एक साथ दो या उससे अधिक pipelined floating-point operations execute कर सकता था, लेकिन उनमें से केवल एक
addinstruction हो सकती थीअगर पिछले result पर निर्भर न होने वाले बहुत सारे additions हों और latency से बचा जा सकता हो, तो multiplier term 1 वाले fused multiply-add instructions भी साथ भेजकर addition throughput को दोगुना किया जा सकता था
वह instruction सामान्य vector floating-point addition के साथ-साथ execute हो सकती थी