सबसे तेज़ branchless binary search
(mhdm.dev)sb_lower_boundstd::lower_boundजैसा ही interface बनाए रखता है, लेकिन जब comparison branch conditional move (cmov) में compile होती है, तब यह सामान्य binary search से अधिकतम 2 गुना तेज़ दिखता है- binary search के comparison result में search position पहले से पता नहीं होती, इसलिए branch prediction miss बार-बार होते हैं, और x86 पर
clang -mllvm -x86-cmov-converter=falseoption इसे कम करने में मदद करता है - यह implementation हर loop में
lengthको आधा करती है और comparison result के आधार पर केवलfirstको update करती है, जिससे instruction count कम होता है, और2^k <= n < 2^(k+1)range में हमेशाk+1comparisons करती है clang -cmovbenchmark में average execution timestd::lower_boundके लिए 61.30ns,sb_lower_boundके लिए 33.24ns, औरbb_lower_boundके लिए 32.73ns था, जबकि geometric mean भी क्रमशः 39.17ns, 19.81ns, 21.33ns था- 8-byte string search जैसे मामलों में जहाँ comparison function धीमा था,
std::lower_boundकभी-कभी थोड़ा आगे रहा, और बड़े arrays में prefetching जोड़ने वाला variantstd::lower_boundसे average में लगभग 2.3 गुना तेज़ था
sb_lower_bound की बुनियादी संरचना
sb_lower_boundstd::lower_boundजैसी ही C++ function है- input हैं
first,last,value,comp - return value वह iterator है जहाँ comparison पहली बार fail होती है, और अगर सभी elements condition satisfy करते हैं तो
lastलौटाया जाता है
- input हैं
- core loop
lengthको आधा करती है, और केवल तबfirstको आगे बढ़ाती है जबcomp(first[length], value)true हो - यहाँ “branchless” का मतलब यह नहीं कि
ifगायब हो जाता है, बल्कि यह कि संबंधितifconditional jump की जगहcmovजैसी conditional move instruction में compile होता है clangमें-mllvm -x86-cmov-converter=falseoption देने पर यह form conditional move में compile हो सकता है
जहाँ std::lower_bound धीमा पड़ता है
- सामान्य binary search middle element और
valueकी तुलना करके left या right interval चुनती है - जब target की position पता नहीं होती, तब
if (comp(first[half], value))अक्सर predict करना मुश्किल branch बन जाती है - CPU branch prediction से अगला instruction पहले चलाने की कोशिश करती है, लेकिन prediction गलत होने पर किया गया काम फेंकना पड़ता है
- conditional move का उपयोग comparison result के आधार पर value चुनते हुए conditional jumps को कम कर सकता है
clang -cmovstd::lower_boundके कुछif/elseको भी conditional move में बदल सकता है, जिससे यह लगभग 25% तेज़ हुआgccमें ऐसी स्थिति में conditional move force करने का अच्छा option नहीं है, औरsb_lower_boundभी अभी optimization level की परवाह किए बिना branchless code emit नहीं करता
comparison count के नज़रिए से “optimal” search
- यहाँ “optimal” का मतलब सबसे कम comparisons वाली binary search है
- size
nकी list मेंstd::lower_boundके possible results element positionsnऔर end position1मिलाकर कुलn+1होते हैं - अगर list size
2^k - 1हो, तो possible results2^kहोते हैं, और हर comparison true/false के रूप में 1 bit information देती है, इसलिए optimal comparison countkहै - length
2^k - 1जैसे “nice” case में बहुत छोटे loop से optimal search संभव है - अगर length मेल नहीं खाती, तो
[0, 1, 2, 3, 4, 5]मेंvalue4 होने जैसी स्थिति में out-of-range access हो सकता है
sb_lower_bound की performance और सीमाएँ
sb_lower_boundजब even length वाले interval को बाँटता है, तब comparison true होने पर भी कुछ मामलों में पर्याप्त elements skip नहीं करता2^k <= n < 2^(k+1)range में यह हमेशाk+1comparisons करता है- उसी range में
std::lower_boundkयाk+1comparisons करता है, और average में लगभगlog2(n+1)comparisons करता है - comparisons ज़्यादा हो सकती हैं, लेकिन loop के अंदर instructions बहुत कम होने से कुल execution time फिर भी तेज़ आता है
- अगर comparison function बहुत धीमा हो, तो
k+1औरlog2(n+1)comparisons का फर्क performance पर असर डाल सकता है gccमें conditional move force करने के लिए x86-specific inline assembly सेcmovइस्तेमाल किया जा सकता है, लेकिन सरल तरीका instruction count बढ़ा देता है और दूसरे तरीके में type-specific assembly अलग से लिखनी पड़ती है
और तेज़ variant bb_lower_bound
bb_lower_boundपहले interval को अलग तरीके से बाँटता है जब तक length2^k - 1form तक नहीं पहुँच जाती, फिर तेज़ दूसरे loop से search करता हैlength & (length + 1)का उपयोग यह पहचानने के लिए होता है कि length11..1, यानी2^k - 1, form में है या नहीं- non-ideal lengths में
auto step = length / 8 * 6 + 1जैसा MAGIC value इस्तेमाल होता है ताकि जल्दी “nice” interval के पास पहुँचा जा सके stepआम तौर परlength / 2से कम नहीं होना चाहिए ताकि तेज़ loop में जल्दी प्रवेश हो, लेकिन अगर यहlengthके बहुत पास हो जाए तो binary search का फायदा खो जाता हैbreakकी वजह सेbb_lower_boundbranch वाला form बन जाता है- सभी lengths के लिए सबसे तेज़
stepकी precomputed table का तरीका अभी तक explore नहीं किया गया है
पूरी तरह branchless implementation ज़रूरी नहीं कि तेज़ हो
- 64-bit machine पर
sb_lower_boundका loop अधिकतम 64 बार चलता है, इसलिएswitchऔर जानबूझकर fall-through का उपयोग करकेlengthcheck भी हटाने वाला “पूरी तरह branchless” version बनाया जा सकता है - इस तरीके में
std::bit_width(length)के जरिए जितनी comparisons चाहिए उतने code position पर jump किया जाता है - वास्तविक performance इससे बेहतर नहीं हुई
- आधुनिक x86 CPUs loop condition जैसी predictable branches को अच्छी तरह handle करती हैं, इसलिए
lengthcheck हटाने का लाभ नहीं मिला - templates, macros, और 64 cases की copy-editing से बचने के लिहाज़ से भी सामान्य loop बेहतर माना गया
benchmark results
- average execution time (ns) में
clang -cmovके आधार पर results इस प्रकार थेstd::lower_: 61.30branchless_lower_: 43.43asm_lower_: 54.32sb_lower_: 33.24sbm_lower_: 35.54bb_lower_: 32.73
- geometric mean execution time (ns) में भी
sb_lower_सबसे कम थाstd::lower_: 39.17branchless_lower_: 25.14asm_lower_: 31.21sb_lower_: 19.81sbm_lower_: 20.91bb_lower_: 21.33
sbm_lower_boundवह variant है जोifकी जगहfirst += comp(first[length], value) * (length + rem)form का उपयोग करकेgccको conditional move generate करने की ओर धकेलता है- यह optimization अगले
gccversion में गायब हो सकती है, इसलिए comment और सावधानी ज़रूरी है - benchmark command में
g++-10,clang++-10,clang++-10 -mllvm -x86-cmov-converter=falseका उपयोग हुआ, और-march=haswellभी जोड़ा गया -march=nativeया-marchन देने से ranking पर बड़ा असर नहीं पड़ा, और test Intel i7 Kaby Lake पर किया गया
branch prediction miss की माप
perfसे मापे गए सामान्यclangrun में लगभग 6.94 अरब branches और लगभग 1.20 अरब branch-misses दर्ज हुए, और branch-misses ratio 17.34% थाclang -cmovrun में लगभग 4.07 अरब branches और लगभग 3.595 करोड़ branch-misses दर्ज हुए, और branch-misses ratio घटकर 0.88% रह गया-cmovने लगभग 2.9 अरब branches और लगभग 1.2 अरब branch failures हटा दिए- हटाई गई branches में prediction miss की संभावना लगभग 41% थी
- यह पूरी तरह unpredictable branches के लिए अपेक्षित 50% के काफ़ी करीब है
धीमे comparison function में नतीजे बदल जाते हैं
- और धीमे comparison function की स्थिति देखने के लिए 8-byte string search का test किया गया
- average execution time (ns) में
std::lower_boundsb_lower_boundसे थोड़ा तेज़ या लगभग बराबर थाgcc:std::lower_160.01,sb_lower_165.66clang:std::lower_157.71,sb_lower_162.68,bb_lower_157.22clang -cmov:std::lower_156.06,sb_lower_164.71,bb_lower_157.48
- इस case में
std::lower_bound,sb_lower_boundसे बहुत थोड़ा लेकिन लगातार तेज़ रहा - library सर्वोत्तम performance के लिए primitive types पर सीधे
sb_lower_boundऔर बाकी मामलों मेंstd::lower_boundका उपयोग कर सकती है
assembly में दिखने वाला अंतर
std::lower_boundकाclang -cmovhot loopcmova,cmovbeजैसी conditional moves शामिल करता है, लेकिन length और position update के लिए कई instructions इस्तेमाल करता हैsb_lower_boundका hot loop half length, remainder, और move होने वाला pointer calculate करकेcmovaसेfirstupdate करता हैbranchless_lower_boundकी assembly बहुत छोटी और साफ़ है, लेकिन performance test मेंsb_lower_boundने कम overhead के साथ बेहतर result दिया
update: और छोटा हुआ sb_lower_bound
- orlp.net author की comment के बाद
sb_lower_boundको इस तरह refactor किया जा सकता है कि hot loop assembly instructions 9 से घटकर 8 रह जाएँ - मुख्य बात यह है कि
length - half,half + length % 2के बराबर होता है - refactored form
half = length / 2calculate करता है, comparison true होने परfirst += length - halfकरता है, और फिरlength = halfसे update करता है clang -cmovमें average execution time लगभग 33ns से घटकर लगभग 32ns हो गया
बड़े arrays में prefetching असरदार रही
- comments में सुझाया गया prefetching ज़रूरी memory को पहले से L1/L2 cache में लाने का तरीका है ताकि वास्तविक access के समय delay कम हो
- उदाहरण latency: L1 लगभग 4 cycles, L2 लगभग 12 cycles, L3 लगभग 40 cycles, memory लगभग 200 cycles
gccऔरclangदोनों__builtin_prefetch()को support करते हैंlength / 4position prefetch करने पर 2 में से 1 व्यर्थ जाता है, औरlength / 8तक जोड़ने पर 6 में से 5 व्यर्थ होते हैं- prefetch position calculation और call का खुद का भी overhead है, और छोटे किए गए hot loop में यह cost महत्वपूर्ण हो जाती है
- कई prefetch strategies 256KB से छोटे arrays में मददगार नहीं थीं
- 256KB से बड़े arrays में prefetching जोड़ने वाला
sbp_lower_boundलगभग 40 लाख entries, यानी 16MB तक के test में average execution time लगभग 32ns से घटकर लगभग 26ns तक ले आया - इसके बाद लगभग 12.8 करोड़ entries, यानी 512MB तक बढ़ाए गए test में prefetch version average time के हिसाब से
std::lower_boundसे लगभग 2.3 गुना तेज़ था- तुलना के लिए
std::lower_boundलगभग 161ns और prefetch version लगभग 71ns था
- तुलना के लिए
बड़े datasets में observations और alternatives
- बहुत बड़े sizes पर
clang -cmovसे बना branchlessstd::lower_bound, branch वाले version से धीमा था - आधुनिक CPUs predicted branch का पीछा करते हुए memory load और speculative execution आगे बढ़ा सकती हैं, जो व्यवहार में prefetching जैसा काम कर सकता है
sbpm_lower_bound,sbm_lower_boundमें prefetching जोड़ने वाला version है, और boolean multiplication सेgccको branchless code generate करने की ओर धकेलता है- 10 लाख से 1 करोड़ elements के बीच performance graph में उछाल दिखा, इसलिए सिद्धांततः इससे भी तेज़ implementation की गुंजाइश है
- लेकिन prefetching code धीरे-धीरे जटिल होता जाता है और magic constants बढ़ते जाते हैं, इसलिए
gcc/libstdc++याllvm/libc++में contribution की संभावना complexity बढ़ने के साथ कम होती दिखी std::lower_boundकी सीमाओं को तोड़ने वाले alternatives में Eytzinger Binary Search शामिल है, जो input array को binary median heap form में rearrange करके cache-friendly lookup देता है- Sergey Slotin at CppCon 2022 के int 16-ary tree test में यह
std::lower_boundसे 7x से 15x तक तेज़ निकला
code और उपयोग की शर्तें
- अगर search या comparison आपके program का सबसे धीमा हिस्सा है, और processor comparison result को predict नहीं कर पाता, तो x86 पर
clangका-mllvm -x86-cmov-converter=falseoption आज़माया जा सकता है - और तेज़ binary search चाहिए तो
sb_lower_boundआज़माया जा सकता है, औरgccमेंsbm_lower_boundभी एक विकल्प है - code MIT license के तहत जारी किया गया है
- code और benchmark github.com/mh-dm/sb_lower_bound/ पर देखे जा सकते हैं
1 टिप्पणियां
Hacker News की राय
जब भी लोगों को branches हटाने की कोशिश करते देखता हूँ, तो सोचता हूँ कि क्या वे जानते हैं कि branch prediction failure से लंबी pipeline का रुक जाना CPU architecture का अनिवार्य हिस्सा नहीं है
pipeline लंबी इसलिए होती है क्योंकि execution से ठीक पहले बहुत सारा analysis और transformation किया जाता है, जबकि यह कोई ऐसा algorithm नहीं है जिसमें state dependency बहुत ज़्यादा हो, इसलिए इसका अधिकांश हिस्सा पहले से किया जा सकता है
Transmeta Crusoe CPU इसी तरह काम करता था, और ऐसी दुनिया की कल्पना की जा सकती है जहाँ branches की चिंता न करनी पड़े
और गहराई से देखें तो हर operation, bit state को देखकर result बदलने वाली एक branch ही है, लेकिन ALU के अंदर की ये local branches main pipeline पर मौजूद branches नहीं होतीं, इसलिए performance को बहुत नुकसान नहीं पहुँचातीं
मुझे याद है कि उस समय srk से भी कहा था कि IPC और throughput में से कौन-सा metric चुनते हैं, यह इस बात को प्रभावित करता है कि आप किसे अच्छा और बुरा मानते हैं
IPC वाला पक्ष मानता है कि अगर higher IPC बनाया जाए तो process technology clock बढ़ा देगी और सब जीतेंगे; throughput वाला पक्ष ज्यादा व्यावहारिक approach लेता है कि Moore's law मर चुका है और silicon को और तेज़ चलाने पर वह पिघल जाएगा, इसलिए ISA को smart तरीके से design करने वाला पक्ष जीतेगा
पिछले 20 वर्षों में दोनों पक्षों ने सफलता और निराशा दोनों देखी हैं, और आजकल RISC-V का CPU architecture में फिर से इसी सवाल पर लौटना दिलचस्प है
instruction set की flexibility के आधार पर modern superscalar ideas कैसे जोड़े जाते हैं, यह follow करने के लिए भी यह अच्छी जगह है, और लंबे समय में मुझे लगता है कि यही पक्ष जीतेगा
Transmeta का translation branch cost को खत्म नहीं करता था
मुझे याद है कि Transmeta में काम करने वाले Linus ने comp.arch thread में कुछ ऐसा कहा था कि “CPU का काम cache misses को जितनी जल्दी हो सके पैदा करना है”
compulsory cache misses मौजूद होते हैं, और कोई भी JIT उन्हें हटा नहीं सकता
असली दुनिया में, आज जैसे बड़े caches होने पर भी capacity misses से बचा नहीं जा सकता
Itanium ने भी माना था कि static analysis से branch cost खत्म की जा सकती है; बस याद कर लें कि उसका नतीजा क्या हुआ
programmer आत्मविश्वास से यह निष्कर्ष निकालने से पहले कि वे modern processors से बेहतर चीज़ आसानी से बना सकते हैं, काश कुछ computer architecture की किताबें पढ़ लें
मेरे हिसाब से वे current processors में लगी बौद्धिक मेहनत के पैमाने को कम से कम 7 अंकों जितना कम आँक रहे हैं
उनमें से एक है process किया जाने वाला input data
binary search इसका बिल्कुल सही उदाहरण है, क्योंकि compiler को पता नहीं होता कि result किस position पर मिलेगा
दूसरा है microarchitecture, खासकर cache hierarchy और execution units की configuration
अगर ISA को ऐसे instructions वाला बना दें जो current CPU के micro-operations जैसे हों, तो हर microarchitecture के लिए फिर से compile करना पड़ेगा
हालांकि इसे तकनीकी रूप से OS JIT से हल किया जा सकता है, जैसे आजकल GPU में programs को bytecode format (DXBC, SPIR-V, NVPTX) में distribute किया जाता है और user-mode GPU driver उन्हें actual hardware instructions में recompile करता है
इससे बड़ा variable यह है कि दूसरे CPU threads ऐसा code चला रहे होते हैं जिसे जाना नहीं जा सकता
hyperthreading हटाकर cores को independent बना दें, तब भी L3 cache, external memory, I/O bandwidth, power और heat जैसे chip-wide shared resources बने रहते हैं
अगर हर चीज़ को Branch™ के रूप में redefine कर दें, तो actual branch न होने वाली चीज़ों सहित कुछ Branch™ पहले से compute की जा सकती हैं
लेकिन आम तौर पर branch removal से मतलब if/else जैसे code में सचमुच calculation path के अलग-अलग हो जाने वाले cases से नहीं होता क्या
ऐसी दुनिया में भी useful optimization संभव होगी, लेकिन वह कई future results को simultaneously calculate करने की कोशिश करने वाली Branch™ तक सीमित होगी
जब भी independently perform किए जा सकने वाले operations होते हैं, उतनी ही उन्हें simultaneously execute करने की संभावना बनती है
मैं सिर्फ decode, fetch और execute की बात नहीं कर रहा
अगर independent ALU और shifter हों, तो add करते समय shift भी किया जा सकता है; और dedicated adder और multiplier हों, तो दोनों को simultaneously try न करने की कोई वजह नहीं है
इसका मतलब है कि आप कई instructions को एक साथ in-flight रखना चाहेंगे, और processing speed से भी तेज़ instructions fetch और decode कर पाने होंगे
साथ ही यह स्वाभाविक रूप से ऐसी स्थिति की ओर ले जाता है जहाँ आप reorder करना चाहेंगे ताकि N Add instructions किसी independent Shift को दिखने से न रोकें
आप सोच सकते हैं कि current structure ज़रूरत से ज़्यादा complex है, और शायद आप गलत भी न हों
फिर भी current structure बनाने में बहुत भारी engineering लग रही है, इसलिए अगर आपको लगता है कि इस तरीके के बिना कहीं तेज़ बनाया जा सकता है, तो यह दावा कितना सही है, इसे गहराई से खंगालना ज़रूरी है
“काश यह सब लिखने के लिए कोई साफ़ और तेज़ bare-metal language होती…” वाले हिस्से में लेखक ने “BUT RUST..” और “BUT ZIG..” फुटनोट डाले हैं, लेकिन सोच रहा हूँ कि Nim कैसा रहेगा
लगता है
lowerBoundका native library implementation मौजूद है: https://github.com/nim-lang/Nim/blob/version-2-0/lib/pure/al...सख्ती से कहें तो यह “bare-metal” language नहीं है, लेकिन C या C++ में compile होती है, इसलिए यह देखना दिलचस्प होगा कि यहाँ किस तरह के code में compile होती है
और यह भी जानना चाहूँगा कि C में आखिर दिक्कत क्या है
TigerBeetle अपनी branchless implementation इस्तेमाल करता है: https://github.com/tigerbeetle/tigerbeetle/blob/e996abcf7154...
ऐसे use case ही वजह हैं कि C++ templates की जरूरत पड़ती है
C साफ़-सुथरी नहीं है
मुझे अभी भी पक्का नहीं है कि यह
lower_boundहै या नहींहो सकता है मैं code गलत पढ़ रहा हूँ, लेकिन duplicates होने पर यह सबसे पहला matching item नहीं, बल्कि कोई भी matching item लौटाता हुआ लगता है
अगर comparison function autocomplete के लिए किसी खास string prefix को खोज रहा हो, तो unique list में भी कई items match कर सकते हैं, और उस स्थिति में हमें list का सबसे पहला item चाहिए होता है
जानना चाहूँगा कि आपको ऐसा क्यों नहीं लगता
काश सभी blog posts इसी तरह शुरू होतीं: “आप व्यस्त होंगे, इसलिए सीधे मुद्दे पर आते हैं। यह रही सबसे तेज़, generic और simple C++ binary search implementation”
Zig standard library binary search के लिए C++ को call नहीं करती
मौजूदा binary search यहाँ है: https://github.com/ziglang/zig/blob/b835fd90cef1447904d3b009...
ठीक से समझ नहीं आ रहा
binary search और branches की समस्या branch खुद नहीं है, बल्कि यह है कि comparison खत्म होने तक आपको नहीं पता होता कि array से अगली कौन-सी memory location fetch करनी है
branch इस्तेमाल करें या कुछ और, फर्क नहीं पड़ता; असली बात यह है कि आप processor से आखिर क्या करवाना चाहते हैं
data dependency है
middle index पढ़ने से पहले आप नहीं जान सकते कि ऊपर वाले range में search करना है या नीचे वाले range में
अनुमान लगाकर दोनों तरफ की reads issue की जा सकती हैं, और तब dependency हल हो जाएगी, लेकिन memory traffic बढ़ जाएगा
यही मुख्य सवाल है कि यह सही trade-off है या नहीं; सिर्फ branch हटाना जवाब नहीं है
article के अंत में इसे cover किया गया है: https://mhdm.dev/posts/sb_lower_bound/#prefetching
इसलिए सच में तेज़ binary search Eytzinger array layout इस्तेमाल करती है: https://algorithmica.org/en/eytzinger
मेरे Cascade Lake processor पर
-mllvm -x86-cmov-converter=falsebinary search performance को लगभग आधा कर देता हैnumbers 100MB
uint32array पर प्रति bsearch nanoseconds में हैंclang 15.0.7 इस खास code optimization में gcc 13.2.1 से काफी खराब दिखता है
assembly यहाँ देखी जा सकती है: https://godbolt.org/z/cbx5Kdjs6
gcc assembly काफी ज्यादा साफ़ दिखती है
100MB इतना बड़ा है कि branch वाला version थोड़ा बेहतर निकलता है, लेकिन इसलिए नहीं कि वह बेहतर है, बल्कि x86 की speculative execution characteristics की वजह से
क्या किसी को पता है कि “BUT RUST” link असल में कहाँ जाना था?
version-pinned नहीं था, इसलिए लगता है पहले ही टूट चुका है, और शायद
starts_withdocumentation comment के बीच में जाना थाlet mid = left + size / 2;[1] https://web.archive.org/web/20230602210213/https://doc.rust-...
Rust के binary search implementation से लिंक करना था
https://doc.rust-lang.org/1.71.1/src/core/slice/mod.rs.html#... पर अपडेट किया गया
दिलचस्प है कि ज़्यादा जटिल
compcomparison function में परिणाम बरकरार नहीं रहतेलेख में ID, फोन नंबर, account, keyword जैसी स्थितियों की कल्पना की गई थी, यानी कुछ हद तक व्यावहारिक binary search scenario जहां comparison function धीमा होता है, और इसलिए 8-byte string search को test किया गया
इस मामले में
std::lower_bound,sb_lower_boundसे बहुत थोड़ा, लेकिन लगातार तेज़ है; और हमेशा best performance पाने के लिए library को primitive types को सीधे handle करते समयsb_lower_bound, और बाकी मामलों मेंstd::lower_boundइस्तेमाल करना चाहिए, ऐसा कहा गया हैयहां का analysis देखना चाहता हूं
अगर data और input सचमुच random हों, तो prediction लगभग आधी बार गलत होगा
CMOV approach comparison function के बाद data dependency की वजह से अटक जाती है
औसतन branch-based approach एक बार में दो comparisons करती है, जबकि CMOV एक करता है, इसलिए जब comparison time branch prediction failure penalty से बड़ा हो जाता है, तो reversal point आने की उम्मीद है
पहले SIMD से मोटे तौर पर बनाया गया मेरा version memory bandwidth से bottleneck होने से पहले
std::lower_boundसे 3 गुना तेज़ था: https://github.com/matthewkolbe/ThinkingInSimd/tree/main/alg...मान लेते हैं कि यह pure random है, लेकिन अगर ये 8-byte strings pure information नहीं हैं, तो modern branch predictors
cmovसे आसानी से बेहतर performance दे सकते हैंलगता है कि
unpredictableattribute अब cmov conversion pass को प्रभावित करता है1 जून तक की स्थिति है, इसलिए शायद clang 17/18 में आएगा: https://reviews.llvm.org/D118118