SearchValues<T> แสดงให้เห็นถึงความได้เปรียบที่วัดผลได้ใน .NET 10 เมื่อต้องสแกนบัฟเฟอร์ขนาดใหญ่เพื่อหาตัวคั่น (delimiters) ที่มีอยู่อย่างเบาบาง โดย SearchValues ใช้เวลาเพียง 3.3 ms เทียบกับ IndexOfAny ที่ใช้เวลา 5.6 ms บนข้อมูลขนาด 32 MB

ทำไมถึงต้องมี SearchValues

.NET 8 ได้แนะนำ SearchValues<T> เพื่อคำนวณชุดตัวอักษร (หรือไบต์) ไว้ล่วงหน้า และปล่อยให้ runtime เลือกอัลกอริทึมการสแกนที่เร็วที่สุดสำหรับชุดข้อมูลนั้น โดยสร้างออบเจกต์เพียงครั้งเดียว แล้วนำกลับมาใช้ใหม่ทุกครั้งที่คุณต้องการค้นหาค่าใดๆ ใน span ซึ่ง runtime อาจใช้คำสั่งแบบ vectorized, ลูปแบบ branch-free หรือเทคนิคระดับต่ำอื่นๆ ที่การเขียนโค้ดขึ้นมาเองแบบแมนนวลไม่สามารถทำได้เทียบเท่าหากไม่ใช้ความพยายามอย่างมาก

ผลการทดสอบ (Benchmarks) ที่สำคัญ

การทดสอบที่เป็นประเด็นในการสนทนาใช้บัฟเฟอร์ขนาด 32 MB ที่มีตัวคั่น 5 ตัวอักษร โดยมีการวัดเวลา 3 วิธีบน .NET 10:

  • IndexOfAny(char[]) – 10.2 ms
  • การใช้ลูป foreach แบบแมนนวลบน span – 19.4 ms
  • SearchValues – 9.7 ms

SearchValues ชนะ IndexOfAny ที่มีมาให้ในตัวด้วยเวลาที่ต่างกันเพียงครึ่งมิลลิวินาทีเท่านั้น ผลลัพธ์นี้ไม่ใช่การเคลมที่เกินจริงว่า "เร็วกว่าห้าเท่า" อย่างที่มีการพูดถึงกันในฟอรัม เพราะ .NET สมัยใหม่มีการปรับแต่ง IndexOfAny สำหรับชุดข้อมูลขนาดเล็กไว้แล้ว ทำให้ช่องว่างความเร็วแคบลง

แต่เมื่อตัวคั่นมีอยู่อย่างเบาบาง เช่น ปรากฏขึ้นเพียงหนึ่งครั้งในทุกๆ 40 KB แทนที่จะเป็นทุกๆ 60 bytes ภาพจะเปลี่ยนไป:

  • IndexOfAny(char[]) – 5.6 ms
  • SearchValues – 3.3 ms

ในกรณีนี้ SearchValues เร็วกว่า 1.7 × เพราะเอนจินสามารถวิ่งผ่านข้อมูลที่ไม่ตรงเงื่อนไขเป็นระยะทางยาวๆ ด้วยขั้นตอนแบบ vectorized ในขณะที่ IndexOfAny จะถอยกลับไปใช้วิธีการที่ไม่ได้รวดเร็วเท่า

ต้นทุนที่แฝงอยู่

SearchValues ไม่ได้มาฟรีๆ การสร้างออบเจกต์ต้องมีการจัดสรรหน่วยความจำและสร้างตารางการค้นหา (lookup tables) ภายใน หากคุณสร้างมันขึ้นมาใหม่ในทุกๆ การเรียกใช้เมธอด ค่าใช้จ่ายส่วนเกิน (overhead) จะกลบความเร็วที่ได้จาก runtime ไปจนหมด ผลการทดสอบที่เรียกใช้รูทีนการสแกนหนึ่งล้านครั้งบนบรรทัดสั้นๆ แสดงให้เห็นว่า:

  • Static (นำกลับมาใช้ใหม่) SearchValues – รวม 25.1 ms
  • SearchValues แบบสร้างใหม่ทุกครั้งที่เรียก – รวม 70.2 ms

การสร้างออบเจกต์ใหม่ทุกครั้งที่เรียกทำให้การทำงานทั้งหมดช้าลงถึงสามเท่าเมื่อเทียบกับการไม่ใช้ SearchValues เลย ดังนั้นควรเก็บอินสแตนซ์ไว้ในฟิลด์ static readonly หรือใช้วิธีการอื่นเพื่อนำกลับมาใช้ใหม่ในการเรียกครั้งต่อๆ ไป

เมื่อไหร่ที่ควรเลือกใช้ SearchValues

  • ข้อมูลขาเข้าขนาดใหญ่ และมีจุดที่ตรงเงื่อนไขน้อย – เส้นทางแบบ vectorized จะโดดเด่นมากเมื่อตัวสแกนสามารถข้ามช่วงข้อมูลยาวๆ โดยไม่เจอตัวคั่นได้
  • การสแกนซ้ำๆ ด้วยชุดข้อมูลเดิม – หากต้องค้นหาตัวอักษรชุดเดิมหลายครั้ง การเสียเวลาตั้งค่าเพียงครั้งเดียวจะคุ้มค่ามาก

เมื่อไหร่ที่ควรใช้ IndexOfAny ต่อไป

  • สตริงสั้นๆ หรือมีจุดที่ตรงเงื่อนไขจำนวนมาก – ค่าใช้จ่ายในการตั้งค่าเพิ่มเติมจะมากกว่าความเร็วที่เพิ่มขึ้นเพียงเล็กน้อย
  • โค้ดที่ใช้ IndexOfAny อยู่แล้ว – การปรับแต่งของ .NET สมัยใหม่นั้นดีเยี่ยมสำหรับชุดตัวอักษรขนาดเล็กอยู่แล้ว ดังนั้นการเปลี่ยนมาใช้ SearchValues อาจไม่เห็นความแตกต่างอย่างมีนัยสำคัญ

ข้อผิดพลาดที่พบบ่อย

  • การสร้างออบเจกต์ไว้ภายใน hot loops – นำไปสู่ความล่าช้าถึงสามเท่าตามที่แสดงไว้ข้างต้น
  • การพยายาม "ฉลาดกว่า" runtime ด้วยการเขียนลูปเอง – เวอร์ชัน foreach แบบแมนนวลนั้นช้ากว่าทั้งสองวิธีที่มีมาให้ในตัวถึงสองเท่า ซึ่งยืนยันว่าไลบรารีของ .NET นั้นยากที่จะเอาชนะได้หากไม่มีความรู้เชิงลึกเกี่ยวกับคำสั่ง SIMD
  • การทึกทักเอาเองว่ามันจะเร็วขึ้นในทุกกรณี – ชัยชนะเพียงครึ่งมิลลิวินาทีบนข้อมูลที่มีความหนาแน่นสูงพิสูจน์ว่า SearchValues ไม่ใช่สูตรโกงที่ใช้ได้กับทุกอย่าง ประโยชน์ของมันขึ้นอยู่กับลักษณะของข้อมูล

บทสรุป

SearchValues<T> จะให้ความได้เปรียบที่ชัดเจนก็ต่อเมื่อคุณสแกนบัฟเฟอร์ขนาดใหญ่เพื่อหาตัวอักษรที่ปรากฏไม่บ่อย และสามารถยอมรับต้นทุนในการสร้างออบเจกต์เพียงครั้งเดียวได้ สำหรับสถานการณ์การค้นหาสตริงทั่วไปส่วนใหญ่ ไม่ว่าจะเป็นข้อมูลขาเข้าสั้นๆ, การเจอจุดที่ตรงเงื่อนไขบ่อยครั้ง หรือโค้ดที่ใช้ IndexOfAny อยู่แล้ว วิธีที่มีมาให้ในตัวยังคงเป็นทางเลือกที่ง่ายและเร็วกว่า จงใช้ SearchValues อย่างเจาะจง เก็บอินสแตนซ์ไว้ในแคช แล้วคุณจะหลีกเลี่ยงบทลงโทษที่แฝงอยู่พร้อมกับได้รับประสิทธิภาพที่แท้จริง

ซอร์สโค้ดและชุดการทดสอบฉบับเต็ม: https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l