1 पॉइंट द्वारा GN⁺ 2024-07-30 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • स्ट्रिंग को कॉपी करते हुए ASCII uppercase अक्षरों को lowercase में बदलने का काम AVX-512-BW से 64-बाइट इकाइयों में प्रोसेस करने का एक प्रयोग है, ताकि छोटी स्ट्रिंग्स में भी SIMD performance निकाली जा सके
  • इम्प्लीमेंटेशन का मुख्य बिंदु यह है कि हर byte के लिए पहले जाँचा जाता है कि वह 'A' से बड़ा या बराबर और 'Z' से छोटा या बराबर है या नहीं, फिर केवल उन्हीं जगहों पर 'a' - 'A' जोड़ने वाला masked operation किया जाता है
  • छोटी स्ट्रिंग्स और लंबी स्ट्रिंग्स के आख़िरी बचे हिस्से को masked load/store से संभाला जाता है, जिससे SIMD code में अक्सर आने वाली छोटे टुकड़ों की processing cost कम होती है
  • Clang 16, Debian 11, AMD Ryzen 9 7950X पर लगभग 1MiB copy को 1 byte से 1KiB chunk तक मापने पर tolower64 तुलना में शामिल विकल्पों में लगातार तेज़ समूह में रहा
  • Zen 4 पर AVX-512-BW ने string processing के लिए अच्छा फिट दिखाया, लेकिन ARM SVE और RISC-V Vector extension को सीधे विस्तार से जाँचा नहीं जा सका

AVX-512-BW से 64-बाइट tolower() बनाना

  • लक्ष्य यह है कि स्ट्रिंग को कॉपी करते समय uppercase ASCII अक्षरों को lowercase में बदलने वाला tolower() kernel SIMD से इम्प्लीमेंट किया जाए
  • AVX-512-BW एक ऐसा extension है जो byte और word स्तर के operations को support करता है, और हाल के AMD Zen processors पर उपलब्ध है
    • AVX-512 कई extensions में बँटा हुआ है, इसलिए support की स्थिति जटिल है
    • खासकर Intel की तरफ़ support को असंगत माना गया है
  • ARM SVE भी string processing के लिए उपयुक्त byte-level masked load/store देता है
    • यह हाल के big-ARM Neoverse cores, जैसे Amazon Graviton, पर उपलब्ध है
    • Apple Silicon पर इसका उपयोग नहीं किया जा सकता
  • RISC-V Vector extension भी ARM SVE जैसी शैली का है, और कई छोटे single-board computers पर उपलब्ध है

tolower64() कैसे काम करता है

  • tolower64() एक AVX-512 आधारित kernel है जो एक बार में 64 bytes प्रोसेस करता है
  • पहले 64 bytes वाले vector register में base values भरी जाती हैं
    • 'A'
    • 'Z'
    • 'a' - 'A'
  • input character vector c को 'A' और 'Z' से compare करके अलग-अलग 64-bit masks बनाए जाते हैं
    • जहाँ c >= 'A'
    • जहाँ c <= 'Z'
  • दोनों masks को _kand_mask64() से जोड़कर is_upper mask बनाया जाता है, जो सिर्फ़ uppercase positions को दिखाता है
  • अंत में _mm512_mask_add_epi8() लागू किया जाता है
    • जहाँ is_upper false है, वहाँ मूल c वैसा ही रहता है
    • जहाँ is_upper true है, वहाँ c + ('a' - 'A') बन जाता है

लंबी और छोटी स्ट्रिंग्स की प्रोसेसिंग

  • लंबी स्ट्रिंग के ज़्यादातर हिस्से को सामान्य unaligned vector load/store से प्रोसेस किया जाता है
    • _mm512_loadu_epi8()
    • tolower64()
    • _mm512_storeu_epi8()
  • छोटी स्ट्रिंग्स और लंबी स्ट्रिंग्स के आख़िरी बचे हिस्से के लिए masked unaligned load/store का उपयोग होता है
  • mask को इस तरह बनाया जाता है कि नीचे की ओर के सिर्फ़ len bits on हों
    • uint64_t len_bits = (~0ULL) >> (64 - len)
    • _cvtu64_mask64(len_bits) से इसे SIMD mask register में डाला जाता है
  • _mm512_maskz_loadu_epi8() mask के off रहने वाले positions पर destination register को 0 से भर देता है
  • _mm512_mask_storeu_epi8() सिर्फ़ उन्हीं positions को store करता है जहाँ mask on है
  • यही तरीका छोटे string fragments को तेज़ी से संभालने की कुंजी है

benchmark की शर्तें और तुलना के लक्ष्य

  • benchmark को Clang 16, Debian 11, AMD Ryzen 9 7950X पर चलाया गया
  • माप का लक्ष्य लगभग 1MiB copy था, और chunk length को 1 byte से 1KiB तक बदला गया
  • source और destination strings के alignment अंतर को दिखाने के लिए उनके बीच कुछ bytes का gap रखा गया, और ये bytes 1MiB measurement में शामिल नहीं थे
  • Ryzen 9 7950X का L2 cache प्रति core 1MiB है, इसलिए उम्मीद थी कि हर test run L3 cache तक जाएगा
  • हर function को inlining और code motion के हस्तक्षेप से बचाने के लिए अलग से compile किया गया
    • वास्तविक code में inlining को रोकने की बजाय प्रोत्साहित करना अधिक संभावित है

नतीजे: tolower64 का स्मूथ performance

  • गुलाबी tolower64 कुल मिलाकर test functions में सबसे तेज़ समूह के लगातार काफ़ी करीब रहा
    • लंबाई 65 bytes होने पर दूसरे vector में जाने से थोड़ा गिरता है
    • यह जल्दी ऊपर जाता है और इसमें गहरी performance valleys नहीं हैं, जो दिखाता है कि masked load/store छोटे string fragments पर प्रभावी है
  • हरा copybytes64 memcpy का वह संस्करण है जो AVX-512 को मिलते-जुलते तरीके से इस्तेमाल करता है
    • यह tolower64 से बहुत ज़्यादा तेज़ नहीं है
    • नया Clang इस function के अर्थ को पहचानकर इसे पूरी तरह rewrite कर देता है, इसलिए इसे Clang 11 से compile किया गया
  • नारंगी copybytes1 byte-level memcpy version है
    • इसे Clang 11 से compile किया गया
    • यह दिखाता है कि 256 bytes से छोटी string fragments पर Clang 11 की auto-vectorization heuristics अपेक्षाकृत अच्छी नहीं हैं
  • लाल tolower <ctype.h> के standard tolower() को call करने वाला baseline है और बहुत धीमा है
  • बैंगनी tolower1 Clang 16 से compile किया गया byte-level tolower() है
    • Clang 16 की auto-vectorization, Clang 11 की तुलना में काफ़ी बेहतर हो गई है
    • फिर भी यह हाथ से लिखे गए version से धीमा है और काफ़ी अधिक जटिल code बनाता है
    • छोटी string fragments की processing tolower64 जितनी अच्छी नहीं है, इसलिए performance graph नुकीले उतार-चढ़ाव दिखाता है
  • भूरा tolower8 पिछली पोस्ट का SWAR tolower() है
    • Clang auto-vectorization की कोशिश करता है, लेकिन function जटिल होने से नतीजा अच्छा नहीं आता
    • इसे Clang 16 से compile किया गया, फिर भी Clang 11 शैली की 256-byte performance cliff दिखाई देती है
  • नीला memcpy glibc के memcpy को call करता है
    • शुरुआत में यह तेज़ है, लेकिन कुछ हिस्सों में copybytes64 की लगभग आधी speed तक गिर जाता है
    • इसका कारण पता नहीं चल सका

निष्कर्ष और code

  • AVX-512-BW strings, खासकर छोटी strings, को संभालने के लिए बहुत उपयुक्त है
  • Zen 4 पर यह बहुत तेज़ है, और intrinsic functions का उपयोग भी अपेक्षाकृत आसान है
  • इसकी सबसे ध्यान खींचने वाली विशेषता स्मूथ performance है
    • छोटी string fragments पर auto-vectorization जब scalar code पर स्विच करती है, तो जो performance valleys दिखती हैं वे यहाँ लगभग नहीं दिखतीं
  • ARM SVE support वाले hardware या RISC-V Vector extension वाले hardware तक सुविधाजनक पहुँच न होने के कारण इन दोनों extensions का विस्तार से अध्ययन नहीं किया जा सका
  • code को वेबसाइट के git repository में देखा जा सकता है

1 टिप्पणियां

 
GN⁺ 2024-07-30
Hacker News की राय
  • unsafe read beyond of death” ट्रिक, hardware में अनुमति होने पर भी Rust और LLVM memory model में undefined behavior मानी जाती है
    बाकी undefined behavior की तरह compiler optimization के दौरान यह मान सकता है कि “ऐसा होता ही नहीं”, जिससे अप्रत्याशित नतीजे आ सकते हैं, और इसे bypass करने के लिए inline assembly इस्तेमाल करनी पड़ती है
    https://github.com/ogxd/gxhash/issues/82

    • ऐसे मामलों में assembly के अलावा कोई विकल्प हो तो अच्छा होगा
      “allocation range से बाहर की values को unspecified elements की तरह पढ़ो, और hardware को दिक्कत हो तभी undefined behavior” जैसे load को support करना कठिन नहीं लगता, और अंदर से वह उसी assembly call का alias भी हो तो पर्याप्त होगा
      इससे आगे, malloc, stack, constants वगैरह हर allocation के बाद कम से कम करीब 64 bytes का fault-free address guarantee हो तो अच्छा होगा, लेकिन इसके लिए कई components का सहयोग चाहिए, इसलिए यह कहीं अधिक जटिल है
      custom allocator में यह मामूली बात है, लेकिन उस स्थिति में custom heap के बाहर के data पर SIMD code इस्तेमाल करना मुश्किल हो जाता है और बहुत छोटी segfault संभावना से बंधना पड़ता है
      sanitizer या Valgrind भी फिर भी उपयोगी हैं। क्योंकि range से बाहर की values को undefined values के रूप में track किया जा सकता है, और actual use पर error दिया जा सकता है
    • hardware level पर भी यह सच में सही है या नहीं, इस पर संदेह है
      unmapped page या protected memory से read करने पर क्या होता है, यह जानना चाहूंगा, और code नहीं देखा इसलिए पता नहीं कि alignment guarantee इससे बचाती है या नहीं
    • “अगर undefined behavior है तो compiler मान सकता है कि ऐसा होता ही नहीं” वाली व्याख्या गलत है
      undefined behavior C standard का technical term है, इसलिए इसे generalize करना ही अजीब है, और ANSI C ऐसी assumption को explicit रूप से allow नहीं करता; ISO C थोड़ा अधिक खुला जरूर है, लेकिन उस assumption को specific रूप से justify नहीं करता
      “UB = यह मान लेना कि हो ही नहीं सकता” जैसी व्याख्या काफी हद तक बेईमान किस्म की डर फैलाने वाली बात लगती है
  • लेख का साफ-सुथरा और performant code देखकर उत्सुकता होती है कि AMD का AVX512 implementation और Intel का प्रस्तावित AVX10 कैसे compete करेंगे
    AVX10 का मुख्य उद्देश्य Intel की P-core/E-core स्थिति को सुलझाना लगता है, जबकि AMD ने स्थिति के हिसाब से Zen5 का full-width implementation या Zen4·Zen5 mobile का 256-bit 2-pass processing तरीका इस्तेमाल करते हुए API को seamless बनाए रखने वाला बेहतर approach चुना लगता है
    लेख में बड़ी performance gains भी सभी Zen4 core से आए results हैं, और AVX512 के कई फायदे हैं, इसलिए Intel ने इसे market segmentation के कारण बहुत ज्यादा restrict करके general-purpose client code में adoption को practically रोक दिया, यह निराशाजनक है

    • अगर Intel आगे आने वाले सभी CPU में AVX10/256 सचमुच डालता है, तो आखिरकार availability के कारण वही जीतेगा
      market ने CPU-specific code path branching को बार-बार reject किया है, और असल में महत्वपूर्ण SIMD implementation lowest common denominator ही होता है
      AVX10.1/256 और AVX512VL में common subset है, इसलिए पर्याप्त समय बीतने पर जब अधिकांश CPU इसे support करने लगेंगे, लोग उसी हिस्से को target करेंगे
      AMD AVX512 support के लिए update किए गए कुछ benchmark apps में आसान जीत हासिल करता रहेगा, लेकिन अगर Intel AVX10 plan बनाए रखता है, तो AMD भी आखिरकार AVX512 compatibility बनाए रखते हुए AVX10/256 को efficiently support करने के लिए 2-pass SIMD pipe को व्यापक रूप से इस्तेमाल कर सकता है
      Intel ने पिछले 10 सालों में कई खराब choices की हैं, लेकिन instruction set से market को बांटना सबसे बुरे फैसलों में से एक था। इसने modern innovations की momentum और interest खुद ही खत्म कर दी, और width से ज्यादा mask operations जैसी capabilities कहीं अधिक महत्वपूर्ण हैं, इसलिए उम्मीद है कि पूरी lineup में AVX10/256 डाला जाएगा
    • Zen 4 का AVX512 implementation double-pumped नहीं है, और tech journalists को इसे ऐसा कहना बंद करना चाहिए
      उस शब्द का एक खास मतलब है, और यह actual behavior से मेल नहीं खाता
      Zen 4 ZMM register operations को कई micro-operations में decode करके खाली 256-bit units पर schedule करता है, और 512-bit full-width shuffle को महंगी emulation से बचाने के लिए dedicated hardware से special-case करता है
      इसलिए 4 256-bit SIMD units वाला Zen 4 भी powerful 2×512-bit core की तरह behave करता है, और यह implementation बिल्कुल भी सस्ता तरीका नहीं है; संभव है कि consumer hardware में अब तक का सबसे अच्छा रूप यही हो
    • समझ नहीं आता कि Intel E-core में double-pumped AVX512 डालकर यह समस्या क्यों हल नहीं करता। या फिर desktop के लिए, जैसा मूल रूप से होना चाहिए, सिर्फ P-core वाला CPU बनाए
      इसे ठीक करने के लिए कई साल मिल चुके हैं, और AMD support करे तब भी market share की वजह से adoption नहीं होता, यह खीझ पैदा करता है; और AVX10 दुर्भाग्य से शायद Intel को दुनिया को और लंबे समय तक रोके रखने देगा
      desktop में मैं बेहतर cores, ज्यादा cores, और wide SIMD·float16·gather/scatter जैसी उपयोगी features खोलने वाला अच्छी तरह standardized instruction set देखना चाहता हूं, और AMD यह काफी अच्छा कर रहा है
      दूसरी तरफ Intel ठीक-ठाक cores के साथ कमजोर cores जोड़ता है, कमजोर cores के हिसाब से अच्छे cores को restrict करता है, कई generations तक समान core count वाले CPU निकालता है, कमजोर cores से ऐसा दिखाता है कि core count ज्यादा है, इतने ज्यादा instruction variants लाता है कि useful common set बनना मुश्किल हो जाता है, और जिन instruction supports का उसने promising संकेत दिया था उन्हें भी छोड़ देता है
      desktop manufacturer preference 90s में Intel, 2000s की शुरुआत में AMD, 2000s के अंत और 2010s में Intel, और अब फिर AMD रहा है। Intel अगर rival को रोकने के अलावा फिर से footing हासिल करना चाहता है तो क्या करेगा, यह देखना दिलचस्प होगा; और competition जारी रहना चाहिए ताकि कोई एक पक्ष बहुत complacent न हो
  • मज़े के लिए देखने लायक सामग्री: http://www.unicode.org/Public/3.1-Update1/CaseFolding-4.txt

    • ऐसी चीज़ भी है: जर्मन ß को uppercase में बदलने पर string की length बदल जाती है
      उदाहरण के लिए "straße".upper() से 'STRASSE' बनता है
      और अगर locale निर्दिष्ट न किया जाए, तो तुर्किक भाषाओं के dotless i को uppercase/lowercase में round-trip convert करते समय 'ı'.upper().lower() से 'i' बन जाता है और गड़बड़ हो जाती है
    • सौभाग्य से यह code DNS काम से निकला है, इसलिए सिर्फ ASCII है और ऐसी जटिलताओं को संभालने की ज़रूरत नहीं
      ASCII case-insensitive protocols कई हैं, और कई servers के hot path में अक्सर दिखाई देते हैं
    • internally ID की तरह इस्तेमाल होने वाली strings और इंसानों द्वारा input किया गया text अलग चीज़ें हैं
      पहले वाले के लिए आम तौर पर 8-bit encoding वाला pure ASCII इस्तेमाल करना ठीक रहता है, लेकिन बाद वाला जटिल हो जाता है
      DNS address इसका आसान उदाहरण है: technically इसमें लगभग पूरा Unicode रखा जा सकता है, लेकिन असली DNS resolution के लिए यह बहुत सीमित ASCII subset में convert होता है, और वह resolution process case-sensitive नहीं होता
      बेशक, ऐसी programming languages भी हैं जो Unicode के सभी writing systems को support करते हुए identifiers को case-insensitive रखती हैं। अगर आप ऐसी चीज़ संभाल रहे हैं, तो मेरी संवेदना
    • जर्मन maße के MASSE में बदलने वाले उदाहरण के संदर्भ में, जर्मन में uppercase Eszett भी है
      यह अभी व्यापक रूप से deploy नहीं हुआ है और इसे support करने वाले fonts भी कम हैं, लेकिन theory में अब यह मौजूद है
  • लगता है लेख में “mask addition” की व्याख्या गलत नहीं है क्या
    सवाल है कि जब is_upper false हो तब जोड़ना और true हो तब वैसे ही copy करना नहीं होना चाहिए?

    • आह, देर से समझ आया कि to_upper variable name उल्टा है और इसे to_lower कहना चाहिए था
      confusing हिस्से की ओर इशारा करने के लिए धन्यवाद, मैंने लेख और code ठीक कर दिया
    • यह operation tolower है
      uppercase A 0x40 है और lowercase 0x60, इसलिए 0x20 जोड़ना तब होना चाहिए जब is_upper true हो
  • ऐसी SWAR optimization अक्सर तभी उपयोगी होती है जब string 8-byte address पर aligned हो
    unaligned string पर SWAR algorithm लगाने से अक्सर यह original algorithm से धीमा हो जाता है
    अगर इसे 3 stages में बाँटा जाए—शुरुआत को aligned address तक process करना, aligned body को process करना, और 8 bytes से कम tail को process करना—तो instructions और बढ़ जाते हैं
    Go में utf8.IsValid के तेज़ होने वाले गलत दावे जैसा एक case और benchmark यहाँ है: https://github.com/sugawarayuuta/charcoal/pull/1

    • AVX-512 और ARM SVE के masked SIMD operations इसी समस्या को हल करने के लिए आए हैं
      memory operations हमेशा aligned और full vector size में किए जाते हैं, लेकिन mask केवल valid elements पर लगाया जा सकता है
      अगर masked vector memory operation unaligned हो और unmapped या protected page को cross करे, तब भी अगर उस lane को mask से off किया गया है तो fault नहीं आता
      strlen() जैसे operations के लिए, जहाँ length पहले से पता नहीं होती, ऐसे special load instructions भी हैं जो fault आने वाले पहले element से ठीक पहले vector length को घटा देते हैं
  • मास्क जोड़ अच्छा दिखता है। .NET intrinsic में AVX512 के mask registers को सीधे manipulate कर पाना अच्छा होता, लेकिन फिलहाल “पहचाने जाने वाले idioms” पर निर्भर रहना पड़ता है
    GCC द्वारा बनाए गए लेखक के core loop को uiCA(CQA/MAQAO) से Ice Lake के हिसाब से analyze करें तो करीब 32B/cycle मिलता है, और 3GHz में बदलें तो, memory bottleneck नहीं है मानकर, लगभग 96GiB/s हो जाता है। बेशक ऐसे algorithms में memory access हमेशा bottleneck होता है
    हालांकि यह optimal utilization के बहुत करीब नहीं लगता, और Clang इस्तेमाल करने पर बेहतर खुले हुए unroll result और बेहतर instruction selection के साथ 42.67B/cycle तक आता है। L2 cache भी शायद ऐसा throughput बनाए रखना मुश्किल पाएगा, लेकिन medium-length strings का case conversion स्क्रीन की रोशनी के cornea तक पहुंचने जितने समय में खत्म हो जाता है, यह दिलचस्प है
    कुछ महीने पहले C# में UTF-8 के अंदर ASCII uppercase/lowercase conversion जैसा कुछ implement किया था: https://github.com/U8String/U8String/blob/main/Sources/U8Str...
    छोटे strings ही ज्यादातर codebases में dominate करते हैं, इसलिए vectorization length से कम के लिए unrolled conversion महत्वपूर्ण है, और switch jump table और branchless fall-through में compile होता है
    अभी सिर्फ 256-bit तक इस्तेमाल कर रहा हूं, क्योंकि Zen 3 या 4 जैसे cases में, जहां केवल 256×4 SIMD units होते हैं, यह पहले ही saturate हो जाता है। C version के साथ side-by-side comparison का example यहां है: https://godbolt.org/z/eTGYhTPan
    AVX512 में vpternlogd से 3-instruction conversion भी संभव लगता है, और जब AVX512 hardware इस्तेमाल कर पा रहा था, तब .NET ने 256-bit width + AVX512VL में ऐसा optimize किया था, लेकिन अब अजीब तरह से 512-bit width में reproduce नहीं हो रहा
    switch dispatch वाले failed SWAR attempts भी दिखेंगे, लेकिन article की license के बारे में जानना है। अगर test suite pass करता है तो इसे इस्तेमाल करना चाहूंगा

    • Clang और GCC का intrinsic handle करने का तरीका अलग है, और खासकर AVX-512 instructions में Clang के Intel guide के specified opcode और algorithm से हटने की संभावना GCC से ज्यादा होती है
      दोनों compilers की structure सोचें तो यह समझ आता है, लेकिन नतीजा कभी improvement होता है और कभी नुकसान
      कुछ साल पहले एक highly vectorized project पर काम किया था जिसे दोनों से compile होना था, और अंत में C reference version के साथ specific targets के लिए inline assembly और .S files repository में maintain करनी पड़ीं
      Makefile गंदा हो गया और test suite में benchmarks तक डालने पड़े, इसलिए maintenance burden बड़ा था; इसी वजह से निष्कर्ष निकला कि intrinsics को auto-vectorization से बेहतर low-level साधन की तरह इस्तेमाल करना बहुत सावधानी से करना चाहिए
      उदाहरण: https://godbolt.org/z/T4Pjhrz5d में GCC output अपेक्षा के मुताबिक था, लेकिन Clang output चौंकाने वाला था और वास्तव में धीमा था। loop में चलाने पर uiCA के हिसाब से GCC के 4 cycles के मुकाबले 7 cycles, और brute-force algorithm में यह function billions बार चलने वाले real app benchmark में भी दिखा
      LLVM codebase में देखने पर याद है कि Clang 16 internal refactoring की वजह से कुछ masked AVX-512 instructions बिल्कुल emit नहीं कर पाने वाली समस्या भी दिखी थी
    • analysis बहुत उपयोगी है
      लक्ष्य maximum possible performance नहीं था; शुरू में बस देखना था कि चलता है या नहीं, लेकिन पहला attempt काफी अच्छा निकला, यह bonus था
      मुख्य रुचि vector register से छोटी strings और throughput graph की घाटियों को हटाने में है
      blog post के अंत में code link follow करें तो license information है, और BIND के लिए मूल रूप से लिखे गए MPL-2.0 हिस्से को छोड़कर यह 0BSD या MIT-0 है
    • बड़े assembly chunk को देखकर साफ नहीं दिखता, लेकिन Clang (x >= 'a' && x <= 'z') को (x - 'a') < ... form में rewrite करके एक instruction कम कर देता है
      अजीब opcode encoding की वजह से कभी-कभी register load भी कम हो जाता है
  • swar क्या है, पता नहीं

    • यह “SIMD Within A Register” का abbreviation है
      आम तौर पर इसका मतलब एक register में कई items को packed करके explicit SIMD instructions के बिना भी effectively SIMD की तरह इस्तेमाल करने की technique है
      उदाहरण के लिए, 64-bit register में 31-bit और 32-bit numbers डालकर carry के लिए 1 bit छोड़ दें, तो एक 64-bit addition से दो additions किए जा सकते हैं
      games में RGB(A) values को 32-bit integer में packing करके graphics में ऐसे tricks इस्तेमाल होते रहे हैं, और ScummVM में भी 32-bit value के अंदर 16-bit RGB pixels के 2, यानी कुल 6 components को interpolate करने वाला code है: https://github.com/scummvm/scummvm/blob/master/graphics/scal...
    • इसका मतलब register के अंदर SIMD है
  • Unicode आने के बाद uppercase और lowercase की concept दलदल बन गई है
    इसे ठीक से करने के लिए बहुत data चाहिए
    अगर आप ऐसा काम कर रहे हैं जिसमें ASCII tolower की execution speed पर समय पर पूरा होना निर्भर है, तो बेहतर है कि कुछ game बदलें और preconditions बदलें

  • पहले एक बार image के आसपास black border डालकर SIMD की buffer से आगे पढ़ने की समस्या पूरी तरह avoid की थी
    यह बहुत अच्छी तरह काम करता था और speed के मामले में कुछ OpenCV implementations को हरा सकता था, लेकिन input को हमेशा इस तरह पूरी तरह control कर पाना संभव नहीं होता

  • उत्सुक हूं कि क्या इसे इस तरह try किया गया है। auto-vectorization का result काफी साफ दिखता है
    https://godbolt.org/z/1c5joKK5n

    • वह मूल रूप से tolower1 जैसा ही है। graph के नीचे वाले bullets देखें