Regex Performance: เทียบ benchmark regex สองพาเทิร์นก่อนขึ้น production
เทียบ regex สองพาเทิร์นแบบ side by side ด้วย Regex Performance Tester — วัด ops/sec เวลาจับคู่เฉลี่ย จำนวน match และดูคำเตือน backtracking ก่อนที่ปัญหาจะไปถึง production
Table of Contents
regex เขียนง่าย แต่ก็พลาดเรื่องประสิทธิภาพได้ง่ายพอกัน พาเทิร์นที่ตอบไวในสายทดสอบสิบบรรทัด อาจทำให้บริการทั้งระบบหยุดชะงักทันทีที่เจอ input ขนาดใหญ่ มั่ว ๆ หรือ input ที่ถูกออกแบบมาโจมตีใน production Regex Performance Tester ถูกสร้างมาเพื่อปิดช่องว่างนี้: วาง sample text ที่ใกล้เคียงของจริง รันสองพาเทิร์นเทียบกันแบบ side by side แล้วอ่านผลเป็นตัวเลขจริง — ops/sec, เวลาจับคู่เฉลี่ย และจำนวน match — พร้อมคำเตือน backtracking ที่ชี้ nested quantifier ที่มีความเสี่ยงแบบ ReDoS ทั้งหมดทำงานในเบราว์เซอร์ ไม่ต้องติดตั้งอะไร และข้อมูลไม่ออกจากเครื่องคุณ
ในบทความนี้ คุณจะได้เห็นว่าเครื่องมือทำงานอย่างไร อะไรทำให้ regex บางตัวช้ากว่ากันมาก และวิธีทำให้การ benchmark สองนาทีกลายเป็นนิสัยใน workflow ประจำวัน ไม่ว่าจะเลือกระหว่างสองพาเทิร์นที่ผลลัพธ์เท่ากัน ตรวจ validator บนเส้นทาง request หลัก หรือชี้ให้เพื่อนร่วมงานเห็นว่าทำไม (\w+)+ ถึงอันตราย การวัดจริงชนะการเดาเสมอ
ทำไมต้องใช้ Regex Performance Tester?
- จับพาเทิร์นที่ช้าก่อนขึ้น production. regex ที่ผ่าน code review มาแล้วอาจพังบน input ที่ไม่มีใครคาดถึง การ benchmark เปลี่ยนคำว่า "ดูเหมือนเร็ว" ให้เป็น ops/sec ที่วัดจากข้อมูลจริงที่ระบบต้องเจอ
- เทียบสองตัวเลือกแบบมีหลักฐาน. ในโค้ดจริงมักมี regex หลายตัวที่ "ใช้ได้" ทั้งคู่ การรัน side by side บน sample เดียวกันตัดเรื่องความรู้สึกออกจากการตัดสินใจ
- เห็นความเสี่ยงแบบ ReDoS ตั้งแต่ยังไม่เกิดเรื่อง. nested quantifier คือส่วนผสมคลาสสิกของ regular expression denial of service คำเตือน backtracking ชี้ให้เห็นความเสี่ยงตั้งแต่ยังอยู่บนหน้าจอ ไม่ใช่ตอนอยู่ในห้อง incident
- ไม่ต้องเซ็ตอัป ทำงานในเบราว์เซอร์. ไม่มี CLI ให้ติดตั้ง ไม่ต้องต่อ benchmark harness ไม่มีการอัปโหลด เปิดหน้าเว็บ วางข้อความ แล้วกดรัน
- วัดด้วยข้อมูลของคุณเอง. ผลบนข้อความสังเคราะห์มักทำให้เข้าใจผิด การวาง log line จริงหรือ payload จริงทำให้ทุกตัวเลขมีความหมายและนำไปใช้ได้
- จำนวน match ช่วยกันผลลวงตา. ถ้าสองพาเทิร์นให้จำนวน match ไม่เท่ากันบนข้อความเดียวกัน แปลว่าทั้งสองไม่เทียบเท่ากัน — ที่เร็วขึ้นแต่พฤติกรรมเปลี่ยนไปคือบั๊ก ไม่ใช่การปรับปรุง
ฟีเจอร์หลัก
| ฟีเจอร์ | ทำอะไร |
|---|---|
| Benchmark สองพาเทิร์น | รันพาเทิร์น A และ B กับ sample เดียวกันภายใต้เงื่อนไขเดียวกัน เทียบกันได้ตรง ๆ |
| ops/sec | นับจำนวนการจับคู่ที่แต่ละพาเทิร์นทำได้ต่อวินาทีบน sample ของคุณ |
| เวลาจับคู่เฉลี่ย | รายงานเวลาเฉลี่ยต่อการรันหนึ่งครั้ง ทำให้ส่วนต่างเล็ก ๆ มองเห็นได้ |
| จำนวน match | ยืนยันว่าทั้งสองพาเทิร์นเจอ match ชุดเดียวกันก่อนเชื่อผลการเทียบความเร็ว |
| คำเตือน backtracking | ตรวจจับ nested quantifier และโครงสร้างที่เกี่ยวข้องกับ catastrophic backtracking แบบ ReDoS |
| รันในเบราว์เซอร์ | benchmark ทำงานในเครื่องทั้งหมด เร็ว เป็นส่วนตัว และไม่ต้องเซ็ตอัป |
บางฟีเจอร์ควรเน้นเป็นพิเศษ คอลัมน์ จำนวน match สำคัญกว่าที่คิด: ต้องยืนยันก่อนว่าทั้งสองพาเทิร์นคืน match ชุดเดียวกัน ไม่เช่นนั้นเท่ากับเทียบแอปเปิลกับส้ม คำเตือน backtracking เป็นการตรวจแบบ static ที่รันคู่กับการจับเวลา พาเทิร์นอาจถูกเตือนแม้บังเอิญรันผ่านเร็วบน sample ที่คุณใส่ และแม้ ops/sec จะเป็นตัวเลขหลักที่คนมักมองหา แต่ เวลาจับคู่เฉลี่ย คือสิ่งที่แปลเป็น latency ที่ผู้ใช้รู้สึกได้ตรงที่สุด
วิธีใช้งาน Regex Performance Tester
- วาง sample text ที่ใกล้เคียงของจริง. ใช้ข้อมูลจริงที่พาเทิร์นจะประมวลผลใน production เช่น log line, API payload หรือข้อความจากฟอร์ม อย่าลืมใส่เคสยาก ๆ เช่น บรรทัดยาว ตัวอักษรซ้ำ ๆ และตัวคั่นที่หายไป
- ใส่พาเทิร์น A และ B. วางพาเทิร์นเดิมไว้ช่องหนึ่ง และตัวท้าทายอีกช่อง ถ้ามีพาเทิร์นเดียว ให้วางเวอร์ชันง่าย ๆ ไว้เทียบด้วย
- กดรัน benchmark. เครื่องมือจะรันทั้งสองพาเทิร์นซ้ำ ๆ กับ sample เดียวกันภายใต้เงื่อนไขเดียวกัน แล้วเก็บข้อมูลเวลา
- เทียบ ops/sec และเวลาจับคู่. ดู throughput ก่อนแล้วค่อยดูเวลาเฉลี่ย ลองคูณเวลาต่อ match ด้วยปริมาณการเรียกจริง เพื่อประมาณผลกระทบใน production
- อ่านคำเตือน. ตรวจคำเตือน backtracking ของทั้งสองพาเทิร์นก่อนประกาศผู้ชนะ — ตัวที่เร็วกว่านิดเดียวแต่มีคำเตือน nested quantifier มักเป็นตัวเลือกที่แย่กว่า
อะไรทำให้ regex ช้า
พื้นฐานเรื่อง backtracking. เอนจิน regex ส่วนใหญ่ รวมถึงของ JavaScript เป็น backtracking engine คือลองเดินหนึ่งเส้นทาง พอ fail จะย้อนกลับไปลองทางเลือกอื่น บน input ปกติต้นทุนส่วนนี้มองไม่เห็น ปัญหาเริ่มเมื่อพาเทิร์นสามารถ match ได้หลายวิธีบนข้อความช่วงเดียวกัน เอนจินจะกลับมาอ่านตัวอักษรชุดเดิมซ้ำ ๆ
nested quantifier เพิ่มงานแบบทวีคูณ. ตัวอย่างตำราเรียนคือ (\w+)+ quantifier ชั้นนอกแยกคำเป็นชิ้นย่อยได้หลายวิธี เวลาจับคู่จึงโตแบบ exponential ตามความยาว input ลองป้อนตัวอักษร a สามสิบตัวตามด้วย ! หนึ่งตัว พาเทิร์นนี้อาจไล่ทางเลือกที่ล้มเหลวหลายล้านทาง นี่คือรูปแบบ catastrophic: quantifier ซ้อนอยู่ใน quantifier บน character class ที่ทับซ้อนกันเอง และตามด้วยสิ่งที่จะไม่ match อีก
ความพังขึ้นกับ input. พาเทิร์นแบบนี้ผ่านทุกเทสปกติได้ ตั้งแต่สาย validation ไปจนถึง payload ตัวอย่าง แต่ระเบิดกับ input ที่ถูกออกแบบมาเพียงชุดเดียว ความไม่สมมาตรนี้ทำให้ ReDoS (Regular Expression Denial of Service) เป็นเรื่องความปลอดภัย ไม่ใช่แค่เรื่อง performance: ผู้โจมตีส่ง input นั้นมาเองโดยตั้งใจ และ request เดียวก็สามารถกิน CPU core ทั้ง core ได้
sample ที่ใกล้ของจริงสำคัญมาก. การ benchmark บนคำว่า "test" ไม่บอกอะไรเลย การ optimize ของเอนจินทำให้สายสั้น ๆ สะอาด ๆ เร็ว ในขณะที่เคสวิบากซ่อนอยู่ ลองใช้ข้อความรูปแบบ production เช่น บรรทัดยาว ตัวอักษรซ้ำ และ input ที่เสียหายบางส่วน ส่วนต่างระหว่างพาเทิร์นจะโผล่มาให้เห็นทันที
ถูกต้องมาก่อน แล้วค่อยเร็ว. เร็วขึ้นไม่มีความหมายถ้าพาเทิร์น match สิ่งที่ไม่ควร match หรือพลาดสิ่งที่ควรจับ ยืนยันว่าจำนวน match ตรงกัน ตรวจพฤติกรรมบน edge case แล้วค่อย optimize พาเทิร์นที่เร็วแต่ผิดเงียบ ๆ แย่กว่าตัวที่ช้าเสมอ
ตัวอย่างการใช้งานจริง
เลือกระหว่างสองพาเทิร์นที่ใช้ได้ผลเหมือนกัน
regex สำหรับ validation ของคุณใช้ได้ แต่อันจาก Stack Overflow ก็ใช้ได้เหมือนกัน วางทั้งสองพร้อม sample จริงแล้วเทียบ ops/sec เมื่อ throughput ต่างกันเป็นเท่าตัว ซึ่งเกิดบ่อยเมื่อพาเทิร์นหนึ่งใช้ character class แน่น ๆ และอีกตัวพึ่ง .* การตัดสินใจจะตอบตัวเอง และคุณมีตัวเลขไว้อ้างใน pull request
ตรวจ validator บนเส้นทาง request หลัก
โค้ด validation รันทุก request ต้นทุน regex จึงคูณด้วย traffic ลอง benchmark validator เดิมกับ request body ในแบบที่แย่ที่สุด เช่น token ยาว เครื่องหมายซ้ำ หรือฟิลด์ที่ fail กลางทาง ถ้า ops/sec แย่หรือมีคำเตือน backtracking บน input แบบนั้น พาเทิร์นตัวนั้นควรอยู่ในลิสต์แก้ก่อนที่เหตุการณ์จะมาหาเอง
เสริมความแข็งแรงให้ไปป์ไลน์ parse log
ตัว parse log ใช้ regex หลายสิบตัวกับทุกบรรทัด มิลลิวินาทีต่อบรรทัดจึงกลายเป็นหลายชั่วโมงต่อสัปดาห์ วาง log จริงสองสามร้อยบรรทัดในช่อง sample แล้ว benchmark พาเทิร์นเป็นคู่ ๆ ตัวที่ช้าที่สุดในไปป์ไลน์มักเป็นเป้า optimize แรกที่คุ้มที่สุด และจำนวน match ช่วยยืนยันว่าเวอร์ชันใหม่ดึง field เดิมมาครบ
สอนมือใหม่ให้เห็นต้นทุนจริงของ regex
บทเรียนที่จำได้แม่นที่สุดคือการเห็น (\w+)+ เดินช้า ๆ บน input ยี่สิบตัวอักษร ในขณะที่เวอร์ชันที่ flatten แล้ววิ่งผ่านไปหมด วางทั้งสองไว้เทียบกัน รัน benchmark บนสายที่ออกแบบให้ backtrack แล้วปล่อยให้ช่องว่าง ops/sec เป็นครูเอง ใช้เวลาสองนาที ประหยัดการบรรยายหนึ่งชั่วโมง
แนวปฏิบัติที่ดี
- ผูก anchor ไว้ก่อน. เริ่มพาเทิร์นด้วย ^ หรือใช้ \b เพื่อให้เอนจิน fail เร็ว แทนที่จะไล่หา match ทุกตำแหน่งในสาย
- ใช้ character class แทน dot-star. [^,]* บอกเจตนาชัดและตัดพื้นที่ค้นหาให้แคบลง ส่วน .* เชิญชวนให้เอนจินสำรวจทางที่ต้องละทิ้งทีหลัง
- ทดสอบกับ input แบบ adversarial. ตัวอักษรเดียวซ้ำยาว ๆ เครื่องหมายคำพูดไม่สมดุล หรือ token ที่ถูกตัดกลางคัน ควรอยู่คู่กับข้อมูลปกติก่อนอนุมัติพาเทิร์นทุกครั้ง
- เก็บ timeout ไว้ใน production. หลาย runtime รองรับ regex timeout หรือ step limit ตั้งค่าไว้เพื่อให้พาเทิร์นวิบากที่ยังไม่รู้จักทำให้ request เดียวเสีย แทนที่จะลาก worker ทั้งตัวลงด้วย
- หลีกเลี่ยง nested quantifier ถ้าไม่จำเป็น. ถ้าหลีกเลี่ยงไม่ได้ ให้ใส่ขอบเขตชัดเจนอย่าง {1,20} และเขียนเหตุผลไว้ในโค้ดด้วย
- benchmark ซ้ำหลังแก้ทุกครั้ง. พาเทิร์นที่ "optimize แล้ว" เปลี่ยนพฤติกรรมได้เสมอ รันการเทียบใหม่บน sample ที่เก็บไว้ทุกครั้งที่แก้พาเทิร์น
ทำสิ่งนี้ให้เป็นนิสัย: เปิด Regex Performance Tester วาง sample จาก production แล้ว benchmark สองพาเทิร์นของคุณแบบ side by side ในไม่กี่วินาที การจับพาเทิร์นที่ช้าหรืออันตรายได้หนึ่งตัวก่อน deploy คุ้มค่าเวลาที่ลงไปหลายเท่าตัว
เครื่องมือที่เกี่ยวข้องที่คุณอาจสนใจ:
- Regex ReDoS Checker — ตรวจพาเทิร์นเดี่ยวเชิงลึกเรื่อง catastrophic backtracking และความเสี่ยง ReDoS
- Regex Tester — ตรวจความถูกต้อง capture group และ flags พร้อมไฮไลต์ match สด ๆ
- Regex Explainer — แปลความหมายพาเทิร์นทีละ token ก่อน commit ลงโค้ด
ขอให้สนุกกับการ benchmark ครับ!
คำถามที่พบบ่อย
ถ: คำเตือน backtracking หมายความว่าอะไร? ตอบ: หมายความว่าเครื่องมือพบโครงสร้างที่เกี่ยวข้องกับ catastrophic backtracking โดยเฉพาะ quantifier ที่ซ้อนอยู่ใน quantifier อีกชั้นบน character class ที่ทับซ้อนกัน พาเทิร์นแบบนี้อาจช้าหรือค้างกับ input บางชุด แม้จะบังเอิญรันผ่านเร็วบน sample ของคุณ จึงควรมองว่าเป็นสัญญาณให้ทำให้พาเทิร์นเรียบง่ายขึ้นหรือใส่ขอบเขตให้ชัดเจน
ถ: ทำไมสองพาเทิร์นถึงได้จำนวน match ไม่เท่ากันทั้งที่ "ใช้ได้" ทั้งคู่? ตอบ: จำนวนที่ไม่เท่าแปลว่าทั้งสองไม่เห็นด้วยกันว่าอะไรคือ match — ตัวหนึ่ง greedy กว่า เข้มงวดกว่า หรือ anchor ต่างกัน ให้แก้ความต่างเชิงพฤติกรรมก่อน เพราะการ benchmark พาเทิร์นที่ไม่เทียบเท่ากันได้แต่คำตอบที่เร็วแต่ผิด
ถ: ผล benchmark จะเท่ากันทุกเบราว์เซอร์และทุกเครื่องไหม? ตอบ: ค่าสัมบูรณ์ต่างกันตามฮาร์ดแวร์และ JavaScript engine จึงควรมอง ops/sec เป็นการเปรียบเทียบเชิงสัมพัทธ์ภายใต้เงื่อนไขเดียวกัน อัตราส่วน A ต่อ B บนเครื่องของคุณสำคัญกว่าตัวเลขสัมบูรณ์เสมอ