Ripgrep: เครื่องมือค้นหาที่เร็วกว่า grep, ag, Git grep และอื่น ๆ (ปี 2016)
(blog.burntsushi.net)- ripgrep (
rg) เป็นเครื่องมือค้นหาบนบรรทัดคำสั่งที่ใช้ Rust ซึ่งผสานความสะดวกในการค้นหาโค้ดแบบ The Silver Searcher เข้ากับประสิทธิภาพดิบระดับ GNU grep และมีไบนารีให้ใช้บน Linux, Mac และ Windows - จากเบนช์มาร์ก 25 รายการ ไม่พบเครื่องมือใดที่เหนือกว่า
ripgrepอย่างชัดเจนทั้งด้านประสิทธิภาพและความถูกต้อง ทั้งในการค้นหาไฟล์ขนาดใหญ่ไฟล์เดียวและการค้นหาไดเรกทอรีขนาดใหญ่ อีกทั้งต้นทุนของการรองรับ Unicode ก็ยังคงต่ำ - ขยายขอบเขตการใช้งานจริงของเครื่องมือค้นหาโค้ด ด้วยการจัดการ
.gitignore, การยกเว้นไฟล์ซ่อนและไฟล์ไบนารีโดยค่าเริ่มต้น, ตัวกรองชนิดไฟล์, การรองรับ PCRE2 แบบเลือกใช้ได้, การค้นหาในหลาย encoding และไฟล์บีบอัด รวมถึงตัวกรองก่อนประมวลผล - ความแตกต่างในการทดลองกับ repository ของ Linux kernel และ OpenSubtitles2016 ขึ้นอยู่กับ literal optimization, การค้นหาหลายแพตเทิร์นด้วย Teddy SIMD, Aho-Corasick, วิธีถอดรหัส UTF-8, การนับบรรทัด และต้นทุนการจัดการ
.gitignoreเป็นอย่างมาก - เมื่อค้นหาไฟล์ขนาดเล็กจำนวนมากแบบขนาน memory map อาจช้าลงได้ ส่วนกับไฟล์ขนาดใหญ่ไฟล์เดียวอาจได้เปรียบ ดังนั้น
ripgrepจึงแยกใช้การค้นหาผ่านบัฟเฟอร์กลางกับการค้นหาผ่าน memory map ตามสถานการณ์
ตำแหน่งที่ ripgrep ตั้งเป้าไว้
ripgrepเป็นเครื่องมือค้นหาบนบรรทัดคำสั่งที่มุ่งรวมความสะดวกของเครื่องมือค้นหาโค้ดเข้ากับประสิทธิภาพของเครื่องมือตระกูลgrep- เครื่องมือที่นำมาเปรียบเทียบคือ
GNU grep,git grep,The Silver Searcher (ag),Universal Code Grep (ucg),The Platinum Searcher (pt)และsift - ประเด็นหลักที่เบนช์มาร์กต้องการตรวจสอบมีสามข้อ
- ไม่มีเครื่องมือใดที่เหนือกว่า
ripgrepอย่างชัดเจนทั้งในการค้นหาไฟล์เดี่ยวและไดเรกทอรีขนาดใหญ่ - ให้การรองรับ Unicode อย่างเหมาะสมโดยไม่ต้องเสียประสิทธิภาพมาก
- เมื่อค้นหาหลายไฟล์พร้อมกัน memory map อาจช้ากว่ามากกว่าจะเร็วกว่าโดยทั่วไป
- ไม่มีเครื่องมือใดที่เหนือกว่า
- ผู้เขียนเป็นผู้สร้าง
ripgrepและเอนจิน regular expression ที่เป็นพื้นฐาน และระบุไว้ว่าเบนช์มาร์กอาจถูกคัดเลือกจนเกิดอคติได้
ฟีเจอร์และพฤติกรรมเริ่มต้น
- ชื่อไฟล์ executable ของ
ripgrepคือrg - การค้นหาโดยค่าเริ่มต้นจะสำรวจไดเรกทอรีปัจจุบันแบบ recursive, เคารพ
.gitignoreและข้ามไฟล์ซ่อนกับไฟล์ไบนารี - รองรับ
.rgignoreด้วย และแพตเทิร์นใน.rgignoreจะมีลำดับความสำคัญเหนือ.gitignore - สามารถใช้
-u,-uu,-uuuเพื่อเพิ่มขอบเขตเป็นการไม่สนใจไฟล์ ignore, รวมไฟล์ซ่อน และรวมไฟล์ไบนารีrg -uuuคล้ายกับgrep -a -r
- รองรับตัวกรองชนิดไฟล์
rg -tpy foo: ค้นหาเฉพาะไฟล์ Pythonrg -Tjs foo: ยกเว้นไฟล์ JavaScript- สามารถเพิ่มกฎชนิดไฟล์ใหม่ได้ด้วย
--type-add
- ยังมีฟีเจอร์หลายอย่างของ
grep- แสดงบริบทรอบผลลัพธ์
- ค้นหาหลายแพตเทิร์น
- ไฮไลต์สี
- รองรับ Unicode เต็มรูปแบบ
- เอนจิน regular expression ค่าเริ่มต้นไม่รองรับ look-around และ backreference แต่หากเลือกเอนจิน PCRE2 ด้วย
-Pก็สามารถใช้ฟีเจอร์เหล่านี้ได้ - รองรับการตรวจจับ UTF-16 บางส่วนอัตโนมัติ และการระบุ encoding ผ่าน
-E/--encoding- รวมถึง UTF-16, latin-1, GBK, EUC-JP, Shift_JIS เป็นต้น
- รองรับการค้นหาไฟล์บีบอัด เช่น gzip, xz, lzma, bzip2, lz4 ด้วย
-z/--search-zip - ยังรองรับตัวกรองก่อนประมวลผลแบบกำหนดเอง เช่น การดึงข้อความจาก PDF, การแตกไฟล์บีบอัดเพิ่มเติม, การถอดรหัส และการตรวจจับ encoding อัตโนมัติ
เหตุผลที่อาจไม่ใช้
- หากให้ความสำคัญสูงสุดกับ portability และการใช้งานได้ทุกที่ grep ที่เป็นไปตามมาตรฐานและติดตั้งแพร่หลายจะเหมาะกว่า
- หากพึ่งพาฟีเจอร์เฉพาะหรือบั๊กของเครื่องมืออื่น
ripgrepอาจไม่เหมาะ - ใน edge case ด้านประสิทธิภาพบางกรณี เครื่องมืออื่นอาจทำงานได้ดีกว่า
- หากติดตั้งไม่ได้หรือไม่มีการรองรับแพลตฟอร์ม ก็ไม่สามารถใช้งานได้เช่นกัน
โครงสร้างการทำงานของเครื่องมือตระกูล grep
- เครื่องมือค้นหาโดยกว้าง ๆ มีสามขั้นตอน
- รวบรวมไฟล์ที่จะค้นหา
- ค้นหาจริง
- แสดงผลลัพธ์
- เครื่องมือตระกูล
grepต้องค้นหาไฟล์ขนาดใหญ่ได้ดี ดังนั้น ประสิทธิภาพของเอนจิน regular expression จึงสำคัญ - เครื่องมือตระกูล
ackต้องจัดการการสำรวจไดเรกทอรีแบบ recursive และการใช้กฎ ignore เช่น.gitignoreได้อย่างรวดเร็ว ripgrepพยายามผสานสองแนวทางเข้าด้วยกัน- เอนจิน regular expression ที่เร็ว
- การค้นหาแบบขนาน
- การกรองเป้าหมายการค้นหา
การรวบรวมไฟล์และการจัดการ ignore
- ในเครื่องมือตระกูล
ackสิ่งสำคัญคือการตัดสินใจอย่างรวดเร็วว่าจะค้นหาไฟล์ใดในไดเรกทอรีปัจจุบัน - ประสิทธิภาพของการเดินไดเรกทอรีได้รับผลจากจำนวนการเรียก
statที่ไม่จำเป็น ripgrepใช้ iterator เดินไดเรกทอรีแบบ recursive ที่ตั้งเป้าให้เรียก system call น้อยที่สุด- การจัดการ
.gitignoreมีต้นทุน- ต้องหาไฟล์ ignore ในแต่ละไดเรกทอรี
- ต้องคอมไพล์แพตเทิร์น ignore
- ต้องนำแพตเทิร์นไปใช้กับพาธผู้สมัครทั้งหมด
- repository ของ Linux kernel มีไดเรกทอรี 4,640 รายการ และไฟล์
.gitignore178 ไฟล์ ripgrepพยายามรองรับความหมายของ.gitignoreให้สมบูรณ์ยิ่งขึ้น และให้ความสำคัญกับแพตเทิร์นที่แมตช์ซึ่งถูกนิยามล่าสุดucgใช้กฎ glob แบบ whitelist แทน.gitignoreจึงอาจเร็วกว่า แต่ก็อาจพลาดไฟล์ที่มีนามสกุลที่ไม่รู้จักได้
ความแตกต่างของเอนจิน regular expression
- เอนจิน regular expression โดยทั่วไปแบ่งเป็นสองประเภท
- แบบอิง backtracking: ฟีเจอร์ครบกว่า แต่อาจช้าลงเป็นเวลาเชิงเลขชี้กำลังกับอินพุตบางแบบ
- แบบอิง finite automata: ฟีเจอร์อาจจำกัดกว่า แต่รับประกันเวลาเชิงเส้นตามความยาวข้อความที่ค้นหา
- เอนจินของแต่ละเครื่องมือมีดังนี้
- GNU grep,
git grep: เอนจินของตัวเองที่อิง finite automata ripgrep: ไลบรารี Rust regex ที่อิง finite automataag,ucg: backtracking ที่อิง PCREpt,sift: ไลบรารี Go regex ที่อิง finite automata
- GNU grep,
agและucgอาจเจอกับพฤติกรรม backtracking กรณีเลวร้ายที่สุดจากการใช้ PCRE- แพตเทิร์นตัวอย่าง
(a*)* cอาจก่อปัญหาในเครื่องมือที่อิง PCRE แต่เครื่องมืออื่น ๆ ที่อยู่ในเบนช์มาร์กจัดการได้โดยไม่มีปัญหา
Literal optimization และ SIMD
- ในการค้นหาสตริงแบบง่าย การปรับแต่งการค้นหา literal อาจสำคัญกว่าเอนจิน regular expression
- Boyer-Moore เป็นอัลกอริทึมค้นหาสตริงย่อยแบบคลาสสิก และสามารถใช้ routine อย่าง
memchrเพื่อค้นหาตำแหน่งผู้สมัครอย่างรวดเร็ว - การใช้งาน
memchrมักตรวจสอบครั้งละ 16 ไบต์ด้วยคำสั่ง SIMD และทำ throughput ได้หลาย GB/s - ไลบรารี Rust regex ดึง literal แบบ prefix และ suffix ออกจากแพตเทิร์นอย่างจริงจัง
foo|bar(a|b)c[ab]foo[yz](foo)?bar(foo)*bar(foo){3,6}
- หาก regular expression ทั้งหมดถูกแยกย่อยเป็น literal เดี่ยวหรือ literal alternation ก็อาจไม่ต้องใช้เอนจิน regular expression หลักเลย
ripgrepใช้ประโยชน์จากลักษณะการแสดงผลลัพธ์แบบรายบรรทัดเพื่อดึง inner literal ด้วย- ตัวอย่าง: ใน
\w+foo\d+จะหาfooก่อน แล้วค่อยตรวจสอบเฉพาะบรรทัดผู้สมัครด้วย regular expression
- ตัวอย่าง: ใน
- สำหรับการค้นหา literal หลายรายการ GNU grep ใช้อัลกอริทึมคล้าย Commentz-Walter ส่วน Rust regex ใช้ Aho-Corasick หรืออัลกอริทึม Teddy SIMD
- Teddy เป็นอัลกอริทึมค้นหาหลายแพตเทิร์นแบบ SIMD ที่มาจาก Intel Hyperscan และเป็นหนึ่งในการปรับแต่งสำคัญที่ทำให้
ripgrepแซง GNU grep
วิธีค้นหา: หลีกเลี่ยงการค้นหารายบรรทัด
- การใช้งานแบบตรงไปตรงมาคืออ่านไฟล์ทีละบรรทัดแล้วนำแพตเทิร์นไปใช้กับแต่ละบรรทัด แต่ในการค้นหาส่วนใหญ่การแมตช์เกิดขึ้นไม่บ่อย จึงไม่มีประสิทธิภาพ
- เครื่องมือค้นหามักค้นหาบัฟเฟอร์ไบต์ขนาดใหญ่ในคราวเดียว
- map ไฟล์เข้าหน่วยความจำ
- อ่านทั้งไฟล์เข้า memory
- ค้นหาแบบค่อยเป็นค่อยไปด้วยบัฟเฟอร์กลางขนาดคงที่
ripgrep, GNU grep และgit grepรองรับการค้นหาแบบค่อยเป็นค่อยไป จึงใช้ได้ทั้งกับไฟล์และ stream- การค้นหาแบบค่อยเป็นค่อยไปทำได้ยาก
- คำนวณเลขบรรทัด
- จัดการกรณีที่บัฟเฟอร์จบกลางบรรทัด
- จัดการบรรทัดยาว
- จัดการ invert match
- จัดการการแสดงบริบทรอบ match
ripgrepยอมรับความซับซ้อนในการ implementation และใช้การค้นหาแบบค่อยเป็นค่อยไป โดยในเบนช์มาร์กให้ผลลัพธ์เร็วกว่า memory map เมื่อค้นหาไฟล์ขนาดเล็กจำนวนมาก
เอาต์พุตและการทำงานแบบขนาน
- ในการค้นหาแบบขนาน หากแต่ละเธรดส่งเอาต์พุตทันที ผลลัพธ์จากไฟล์ต่าง ๆ อาจปะปนกันได้
- เครื่องมือค้นหาโค้ดแบบขนานทั้งหมดจะเขียนผลการค้นหาลงในบัฟเฟอร์กลางในหน่วยความจำ และทำให้เฉพาะขั้นตอนการส่งออกเป็นแบบลำดับ
- วิธีนี้ทำให้เธรดค้นหาสามารถทำการค้นหาจริงแบบขนานได้
- ข้อเสียคือในกรณีอย่างไฟล์ขนาด 2GB ที่ทุกบรรทัดแมตช์กัน อาจใช้หน่วยความจำมากขึ้น
ripgrepจะเขียนตรงไปยังstdoutโดยไม่ใช้บัฟเฟอร์กลางเมื่อค้นหาจากstdinหรือไฟล์เดี่ยว
ระเบียบวิธีเบนช์มาร์ก
- เบนช์มาร์กแบ่งตามปัญหาของผู้ใช้ปลายทาง
- การค้นหาในที่เก็บโค้ดขนาดใหญ่
- การค้นหาในไฟล์ขนาดใหญ่ไฟล์เดียว
- รูปแบบการค้นหาเอนเอียงไปทางลิเทอรัลแบบง่าย, alternation และเรกิวลาร์เอ็กซ์เพรสชันแบบเบา
- เนื่องจากเครื่องมือแต่ละตัวมีพฤติกรรมเริ่มต้นต่างกัน จึงพยายามปรับเงื่อนไขอย่างหมายเลขบรรทัด, Unicode,
.gitignoreและ whitelist ให้ตรงกันเพื่อให้เปรียบเทียบอย่างเป็นธรรม - เวอร์ชันที่ใช้ในเบนช์มาร์กมีดังนี้
ripgrepv0.1.2- GNU grep v2.25
git grepv2.7.4agcommitcda635, PCRE 8.38ucgcommit487bfb, PCRE 10.21 JITptcommit509368siftcommit2d175c
ackถูกตัดออกเพราะในตอนนั้นช้ากว่าเครื่องมืออื่นมาก- ตัวรันเบนช์มาร์กคือ
benchsuiteซึ่งต้องใช้ Python 3.5 ขึ้นไป และรวมอยู่ในที่เก็บripgrep - แต่ละคำสั่งรัน warm-up 3 ครั้งก่อนการวัด เพื่อให้คอร์ปัสถูกโหลดขึ้น OS page cache
- แต่ละคำสั่งวัด 10 ครั้ง และบันทึกค่าเฉลี่ยกับส่วนเบี่ยงเบนมาตรฐาน
- สภาพแวดล้อมที่รันคือ Amazon EC2
c3.2xlarge, Ubuntu 16.04, Xeon E5-2680 2.8GHz, หน่วยความจำ 16GB, SSD 80GB - มีการเผยแพร่ล็อกการตั้งค่า ผลลัพธ์สรุป และ raw CSV ด้วย
ผลการค้นหาโค้ดเคอร์เนล Linux
- เบนช์มาร์กการค้นหาโค้ดรันบนที่เก็บเคอร์เนล Linux ที่ build แล้ว commit
d0acc7 - เหตุผลที่ใช้ที่เก็บเคอร์เนลที่ build แล้ว เพราะผลลัพธ์จากการ build อาจคงอยู่ในที่เก็บและส่งผลต่อความเกี่ยวข้องของผลการค้นหาและประสิทธิภาพได้
- ใน
linux_literal_defaultการค้นหาลิเทอรัลแบบง่ายPM_RESUMEเผยให้เห็นความแตกต่างของพฤติกรรมเริ่มต้นของแต่ละเครื่องมือrgเคารพ.gitignoreและข้ามไฟล์ซ่อนกับไฟล์ไบนารีagและptก็คล้ายกัน แต่จะนับจำนวนบรรทัดด้วยucgไม่อ่าน.gitignoreและค้นหาโดยอิง whitelistsiftโดยพื้นฐานค้นหาแทบทุกอย่างgit grepมีข้อได้เปรียบจากการได้ชุดไฟล์ที่จะค้นหาจาก git index
- การเคารพ
.gitignoreช่วยเพิ่มความเกี่ยวข้องของผลลัพธ์ แต่อาจมีต้นทุนด้านประสิทธิภาพ - ใน
linux_literalค่าrg (whitelist)แสดงประสิทธิภาพแทบเท่ากับucgและrg (ignore)อยู่ในระดับใกล้เคียงกับgit grep rg (ignore) (mmap)และag (ignore) (mmap)ช้าลงจากการใช้ memory map และภายใต้เงื่อนไขเดียวกันrg (ignore)เร็วกว่ามาก- บนเครื่องโลคัล เวอร์ชันที่ใช้ memory map ก็ช้ากว่าเช่นกัน แต่ส่วนต่างน้อยกว่าบน EC2
Unicode และการค้นหาแบบไม่แยกตัวพิมพ์ใหญ่เล็ก
- ใน
linux_literal_caseiptช้าลงอย่างมากเพราะจัดการ-iเป็น(?i)ของ Go regexp siftช้าลงน้อยกว่าเพราะใช้วิธีแปลงแพตเทิร์นและบล็อกค้นหาเป็นตัวพิมพ์เล็ก แต่การปรับให้เหมาะสมนี้รองรับเฉพาะตัวพิมพ์ใหญ่เล็กแบบ ASCII จึงไม่ถูกต้องสำหรับการจัดการตัวพิมพ์ใหญ่เล็กของ Unicoderipgrepแปลงการค้นหาแบบไม่สนตัวพิมพ์ใหญ่เล็กให้เป็นชุดลิเทอรัลที่เป็นไปได้ และใช้ Teddy เพื่อหาตำแหน่งผู้สมัครอย่างรวดเร็ว- การค้นหา
\wAhในlinux_unicode_wordตรวจสอบว่า\wที่รับรู้ Unicode จับผลลัพธ์อย่างµAhได้หรือไม่ - มีเพียง
rgและgit grepที่สลับ Unicode ได้ ส่วนag,pt,sift,ucgใช้\wแบบ ASCII เท่านั้น - เมื่อเปิดการรองรับ Unicode
git grepมีต้นทุนด้านประสิทธิภาพสูง แต่ripgrepแทบไม่มีประสิทธิภาพลดลง ripgrepรวมการถอดรหัส UTF-8 ไว้ใน finite state machine จึงแมตช์กับสตริงไบต์ UTF-8 โดยตรงโดยไม่ต้องมีขั้นตอนถอดรหัสแยกต่างหาก
ความแตกต่างตามความซับซ้อนของเรกิวลาร์เอ็กซ์เพรสชัน
- สำหรับเรกิวลาร์เอ็กซ์เพรสชันที่มี suffix เป็นลิเทอรัลอย่าง
[A-Z]+_RESUMErgและucgใช้_RESUMEเพื่อหาผู้สมัครอย่างรวดเร็ว - สำหรับลิเทอรัล alternation อย่าง
ERR_SYS|PME_TURN_OFF|LINK_REQ_RST|CFG_BME_EVTripgrepใช้ Teddy และอาจไม่ใช้เอนจินเรกิวลาร์เอ็กซ์เพรสชันหลักเลย - ใน alternation แบบไม่สนตัวพิมพ์ใหญ่เล็ก
ripgrepก็สร้าง prefix ของชุดตัวพิมพ์ใหญ่เล็กเพื่อหา candidate ด้วย Teddy แล้วตรวจสอบเฉพาะ candidate ด้วยเรกิวลาร์เอ็กซ์เพรสชันเต็ม - ในการค้นหา
\p{Greek}มีเพียง Rust regex และ Go regex ที่รองรับพร็อพเพอร์ตี Unicode นี้ และrgเร็วกว่าpt,siftมาก - ในการค้นหา
\p{Greek}แบบไม่สนตัวพิมพ์ใหญ่เล็กsiftรายงานแมตช์ไม่ได้ และptจัดการตัวพิมพ์ใหญ่เล็กของ Unicode ไม่ถูกต้อง - สำหรับแพตเทิร์นที่ไม่มีลิเทอรัลอย่าง
\w{5}\s+...ประสิทธิภาพของ regex engine จะแสดงออกมาโดยตรงrgยังค่อนข้างเร็วแม้อยู่ในสถานะรองรับ Unicodegit grepมีต้นทุนสูงเมื่อรองรับ Unicode- Unicode DFA จัดการชุดสถานะ NFA ที่ใหญ่กว่า ASCII DFA มาก โดยตัวเลขตัวอย่างคือ ASCII ประมาณ 250 สถานะ NFA และ Unicode ประมาณ 77,000 สถานะ NFA
การค้นหาในไฟล์ขนาดใหญ่ไฟล์เดียว
- เบนช์มาร์กไฟล์เดี่ยวใช้ตัวอย่าง OpenSubtitles2016
- ตัวอย่างภาษาอังกฤษมีขนาดประมาณ 1GB
- ตัวอย่างภาษารัสเซียมีขนาดประมาณ 1.6GB
- ในพื้นที่นี้ ประสิทธิภาพของ regex engine และการปรับลิเทอรัลให้เหมาะสมมีความสำคัญมากขึ้น
- ใน
subtitles_literalการค้นหาSherlock HolmesและШерлок Холмсทั้งคู่rgเร็วที่สุด ripgrepพยายามเลือก ไบต์ที่พบได้เบาบาง จากลิเทอรัลเพื่อใช้กับmemchr- การใช้งาน Boyer-Moore มาตรฐานมักใช้ไบต์สุดท้ายในการค้นหาผู้สมัคร
rgพยายามเลือกไบต์ที่พบน้อยกว่า เพื่อให้ข้ามในลูปที่ปรับด้วย SIMD ได้นานขึ้น
- แพตเทิร์นภาษารัสเซียใน UTF-8 มีอักขระจำนวนมากที่ขึ้นต้นด้วย
\xD0หรือ\xD1ทำให้การค้นหาไบต์แรกอาจไม่มีประสิทธิภาพ rgใช้ตารางความถี่ 256 ไบต์ที่คำนวณไว้ล่วงหน้า เพื่อเลือกไบต์ที่พบน้อยกว่าแทน\xD0,\xD1- สำหรับไฟล์ใหญ่ไฟล์เดียว เพียงสร้าง memory map ครั้งเดียวก็พอ ดังนั้นการค้นหาแบบ memory map ของ
rgจึงเร็วกว่าการใช้rg (no mmap)ประมาณ 25%
Unicode และ alternation ในไฟล์เดี่ยว
- ใน
subtitles_literal_caseirgรวดเร็วแม้จะจัดการการค้นหาแบบไม่สนตัวพิมพ์ใหญ่เล็กของ Unicode ได้ถูกต้อง - GNU grep มีต้นทุนสูงในการค้นหาแบบไม่สนตัวพิมพ์ใหญ่เล็กของ Unicode
- ในการค้นหาภาษารัสเซียแบบไม่สนตัวพิมพ์ใหญ่เล็ก
grep (ASCII)ดูเหมือนจะเพิกเฉยต่อ-iในทางปฏิบัติ และagรายงานแมตช์ 0 รายการ - ใน
subtitles_alternateการค้นหา alternation ของชื่อตัวละครหลายชื่อrgเร็วที่สุดทั้งภาษาอังกฤษและรัสเซีย - สำหรับ alternation ภาษาอังกฤษ
rgเร็วกว่า GNU grep ประมาณหนึ่งหลักลำดับขนาด - ใน
subtitles_alternate_caseirgช้าลงมากกว่าเดิม แต่ยังนำหน้าเครื่องมืออื่นในภาษาอังกฤษ - ในกรณีนี้ candidate ลิเทอรัลมีจำนวนมากเกินกว่าที่ Teddy จะรับไหว
rgจึงเปลี่ยนไปใช้ Aho-Corasick แทน Teddy ripgrepใช้ Aho-Corasick แบบ “advanced” ที่อิง transition table เพื่อทำหนึ่ง transition ต่อไบต์อินพุต
inner literal และแพตเทิร์นที่ไม่มี literal
- แพตเทิร์นอย่าง
\w+\s+Holmes\s+\w+ถูกสร้างขึ้นเพื่อหลีกเลี่ยงการปรับแต่ง prefix/suffix literal แต่ยังสามารถใช้ประโยชน์จาก inner literalHolmesได้ ripgrepและ GNU grep ทำการปรับแต่งแบบ inner literalripgrepใช้regex-syntaxของ Rust regex เพื่อดึง literal ออกจาก AST ของแพตเทิร์น- ในเวอร์ชันภาษารัสเซีย
\w+\s+Холмс\s+\w+มีเพียงเครื่องมือที่รองรับ Unicode อย่างถูกต้องเท่านั้นที่ให้ผลลัพธ์ได้อย่างมีความหมาย - สำหรับแพตเทิร์นยาวที่ไม่มี literal เลยอย่าง
\w{5}\s+...นั้นrgอยู่ในกลุ่มที่เร็วที่สุดสำหรับภาษาอังกฤษ ส่วน GNU grep เวอร์ชันที่รองรับ Unicode ใช้เวลามากกว่า 90 วินาทีในภาษาอังกฤษ และมากกว่า 4 นาทีในภาษารัสเซีย จึงถูกตัดออก ripgrepรักษาการรองรับ Unicode พร้อมคงประสิทธิภาพไว้ได้ด้วยการรวมการถอดรหัส UTF-8 เข้าไปใน DFA
เบนช์มาร์กเพิ่มเติม
everythingเป็นการทดสอบที่ไม่สมจริง ซึ่งจับคู่ทุกบรรทัดในรีโพซิทอรี Linux ด้วย.*rgรายงาน 22,065,361 บรรทัดภายใน 1.081 วินาทีagและptไม่ได้รายงานทุกบรรทัด จึงดูเหมือนว่ามีการจำกัดจำนวนแมตช์
nothingเป็นการทดสอบที่ใช้ invert match กับ.*เพื่อไม่รายงานบรรทัดใดเลยrgทำเวลาได้ 0.302 วินาที ส่วนgit grepทำได้ 0.905 วินาทีptและucgไม่รองรับ invert search
contextพิมพ์บริบท 2 บรรทัดรอบ ๆSherlock Holmesจากคอร์ปัสซับไตเติลภาษาอังกฤษrgทำเวลาได้ 0.612 วินาที ส่วนsiftทำได้ 0.717 วินาที ซึ่งใกล้เคียงกันucgไม่รองรับฟีเจอร์นี้
hugeค้นหาSherlock Holmesในซับไตเติลภาษาอังกฤษทั้งหมดขนาด 9.3GBrgทำเวลาได้ 1.786 วินาที, GNU grep 5.119 วินาที และsift3.047 วินาทีucgรายงานเพียง 1,543 บรรทัดภายใต้เงื่อนไขการนับบรรทัด จึงให้ผลลัพธ์ผิดพลาด และคาดว่าเกิดปัญหาในการค้นหาไฟล์ที่มีขนาดเกิน 2GB
สรุป
ripgrepไม่ได้ชนะทุกเบนช์มาร์กในการค้นหารีโพซิทอรีเคอร์เนล Linux เสมอไป แต่ในแง่ประสิทธิภาพและความถูกต้อง ก็ยากที่จะบอกว่าเครื่องมืออื่นเหนือกว่าอย่างชัดเจนgit grepอาจเร็วกว่าได้ไม่กี่มิลลิวินาทีในบางกรณีที่เรียบง่าย แต่เมื่อแพตเทิร์นซับซ้อนขึ้นหรือจำเป็นต้องใช้ Unicode ก็มีบางกรณีที่ripgrepนำห่างอย่างมาก- ปัจจัยต่อไปนี้มีส่วนช่วยต่อประสิทธิภาพการค้นหาโค้ดของ
ripgrep- การไล่ดูไดเรกทอรีที่รวดเร็วโดยตั้งเป้าให้เรียก
statให้น้อยที่สุด - การแมตช์ glob ของ
.gitignoreด้วยRegexSet - การกระจายงานผ่าน Chase-Lev work stealing queue
- การเลือกไม่ใช้ memory map เมื่อค้นหาไฟล์ขนาดเล็กจำนวนมาก
- เอนจิน regular expression ที่รวดเร็ว
- การไล่ดูไดเรกทอรีที่รวดเร็วโดยตั้งเป้าให้เรียก
- ในการค้นหาไฟล์เดี่ยว
ripgrepเร็วที่สุดในเบนช์มาร์กหลักทั้งหมด หรือไม่ก็นำหน้าด้วยส่วนต่างมาก - ประสิทธิภาพของไฟล์เดี่ยวได้รับอิทธิพลจาก
memchrแบบ sparse byte, Teddy SIMD, Aho-Corasick และ DFA ที่ฝังการถอดรหัส UTF-8 ไว้ในตัว - ในเบนช์มาร์กที่ต้องใช้ฟีเจอร์ Unicode มีเพียง
rg, GNU grep และgit grepเท่านั้นที่แสดงการรองรับอย่างมีความหมาย และโดยทั่วไป GNU grep กับgit grepต้องจ่ายต้นทุนด้านประสิทธิภาพสูง - บน Linux x86_64 memory map เสียเปรียบในการค้นหาไฟล์ขนาดเล็กจำนวนมากแบบขนาน แต่ได้เปรียบในการค้นหาไฟล์ขนาดใหญ่ไฟล์เดียว และในสภาพแวดล้อม VM อาจมี penalty เพิ่มเติม
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เร็วจริง และยังคงแนะนำให้ใช้ร่วมกับ fzf อยู่เสมอ
ใช้เป็นฟังก์ชัน PowerShell ที่ค้นหาด้วย
ripgrepก่อน จากนั้นซ้อนการค้นหาแบบ fuzzy บนไฟล์ผลลัพธ์+ข้อความ และใช้batแสดงบริบทในโปรเจกต์ที่มีหลาย repository ปะปนกันอยู่ สามารถค่อย ๆ จำกัดขอบเขตได้เร็วมากในกรณีที่ “รู้ว่ามันอยู่ที่ไหนสักแห่ง แต่ไม่รู้ตำแหน่งหรือชื่อที่แน่ชัด”
วิธีนี้มาจาก https://github.com/junegunn/fzf/blob/master/ADVANCED.md และแม้จะไม่ได้ใช้ทั้งหมด ก็ควรลองอ่านผ่าน ๆ เพื่อเก็บไอเดีย
fzfทำให้ค้นหาแบบ fuzzy ได้ไม่ใช่แค่ไฟล์ข้อความ แต่รวมถึงไฟล์หลายรูปแบบอย่าง PDF, zip ด้วย
รายละเอียดอยู่ที่ https://github.com/phiresky/ripgrep-all/wiki/fzf-Integration
วิธีคือเลือกผลลัพธ์จาก
rgด้วยfzfแล้ว parse ไฟล์กับเลขบรรทัดที่เลือก จากนั้นเปิดด้วย$EDITOR +"${linenumber}" "$file"เหมือนบดกาแฟด้วยมือแทนที่จะใช้เครื่องบดไฟฟ้า
fzfจะเลือกไฟล์จำนวนมากเพื่อเพิ่มเข้า Git พร้อมข้ามบางไฟล์ได้ถ้าใส่
fza = "!git ls-files -m -o --exclude-standard | fzf -m --print0 | xargs -0 git add"ไว้ใน[alias]ของgitconfigก็จะใช้git fzaเพื่อแสดงรายการไฟล์ที่ถูกแก้ไขหรือยังไม่ได้เพิ่ม แล้วกดสเปซเพื่อ toggle รายการและเลื่อนไปยังรายการถัดไปได้alias นี้กับ fzf+fd ช่วยเร่งบางส่วนของ workflow ได้มากพอสมควร
ยังมีคู่มือที่รวบรวมสิ่งที่ควรใส่ในค่า zsh บน macOS ด้วย: https://gist.github.com/aclarknexient/0ffcb98aa262c585c49d4b...
ripgrepในวิธีเกือบเหมือนกันใช้เป็น จุดเริ่มต้น เพื่อจำกัดขอบเขตไฟล์หรือโปรเจกต์ใน codebase ที่มี repository หลายร้อยตัว แล้วค่อยเจาะลึกต่อ
ใน Emacs ผมใช้
ripgrepผ่านแพ็กเกจ project.el และ dumb-jumpอาจไม่ใช่วิธียอดนิยมที่สุด แต่ประสบการณ์โดยรวมค่อนข้างน่าพอใจ
แค่ติดตั้ง
dumb-jumpด้วยpackage-installแล้วตั้งค่า(add-hook 'xref-backend-functions #'dumb-jump-xref-activate)ก็พอในโปรเจกต์ Python เมื่อใช้
M-.หรือC-u M-.เพื่อค้นหานิยามของ identifier,dumb-jumpจะรันคำสั่งrgให้เหมาะกับโปรเจกต์และชนิดไฟล์ปัจจุบัน แล้วแสดงผลใน Xref bufferรองรับ
agด้วย และถ้าไม่มีagหรือrgก็จะ fallback ไปใช้grepซึ่งเวลาค้นหาทั้ง home directory ก็อาจช้าตามที่คาดไว้ripgrepได้ค่อนข้างง่ายด้วย project.el ที่มากับ Emacs อยู่แล้วไม่จำเป็นต้องมีแพ็กเกจภายนอกเสมอไป และถ้าต้องการใช้แทน
grepที่ช้าในไดเรกทอรีใหญ่ ๆ ก็ตั้งค่า(setq xref-search-program 'ripgrep)ได้จากนั้นการค้นหาในโปรเจกต์อย่าง
C-x p g foo RETจะถูกรันในโปรเจกต์ปัจจุบันในรูปแบบrg -i --null -nH --no-heading --no-messages -g '!*/' -e fooผลลัพธ์จะปรากฏใน Xref buffer ทำให้ใช้คีย์อย่าง
n,p,RET,C-oเพื่อไป match ถัดไป/ก่อนหน้า, jump ไปซอร์ส, หรือแสดงในหน้าต่างแบ่งได้สะดวกripgrepมองว่า regex นั้นผมยังไม่ได้ลองรันเอง แต่คิดว่าน่าจะตัด flag --pcre2 ออกได้assertion
\bตัวที่สองและสามก็น่าจะตัดออกได้เช่นกัน ส่วนตัวแรกอาจจำเป็นripgrepและมี binding ของevil-collectionด้วย จึงใช้งานได้อย่างน่าพอใจ: https://github.com/Wilfred/deadgrepซึ่งเป็นสถานการณ์ที่เดิมทีคงใช้
rgrepจุดที่น่าสนใจคือ การค้นหาของ VS Code ตอนนี้ก็ทำงานผ่าน
ripgrepด้วย wrapper ของ Node.js แล้วhttps://www.npmjs.com/package/@vscode/ripgrep
ripgrepไม่ได้ สิ่งนี้ดีมากสามารถหาไบนารี
rgได้ภายในเส้นทางติดตั้งของ VS อย่างน้อยในสภาพแวดล้อม Windows ที่ทำงานของผมก็ทำได้ใช้
ripgrepมาประมาณ 2 ปี และตอนนี้กลายเป็นเครื่องมือที่ขาดไม่ได้แล้วเหตุผลหลักที่ย้ายจาก
grepคือ ความสะดวกในการใช้งานโดยค่าเริ่มต้นมันเคารพกฎ
.gitignoreและข้ามไฟล์/ไดเรกทอรีที่ซ่อนอยู่ รวมถึงไฟล์ไบนารี ทำให้rg search_term directoryดีกว่าคำสั่งgrepที่เทียบเท่ากันมาก ส่วนความเร็วที่เพิ่มขึ้นถือเป็นโบนัสเวลาที่ match ยาวเกินไปจนทำให้ terminal เละ ผมมักใช้ ตัวเลือก -M อย่าง
-M 1000-Mยอดเยี่ยมจริง ๆสะดวกเป็นพิเศษเมื่อต้องการละเว้นผลลัพธ์จากไฟล์ minified ที่ไม่อยากเห็น และการใช้ ตัวเลือก -g อย่าง
-g *.csเพื่อค้นหาเฉพาะไฟล์นามสกุลหนึ่งก็ใช้ได้ดีการเป็นไบนารีแบบ standalone ที่พกพาได้ก็มีประโยชน์เช่นกัน เวลาใช้เครื่องใหม่ก็แค่ใส่ไฟล์ executable เข้าไป แล้วตั้ง alias ของ
grepให้เป็นrgถึงพิมพ์grepตามความเคยชินก็จะรันrgแทนในปี 2023 ก็อาจยังเป็นคำพูดที่ถูกอยู่ แต่ปัญหาคือเครื่องมือทดแทน
grepที่ทำงานแบบขนาน เช่นripgrepหรือagเร็วกว่าgrepเดิมมาก จนความต่างด้านความเร็วเล็กน้อยระหว่างกันแทบใช้เป็นเกณฑ์แยกแยะได้ยากผมใช้
agใน Emacs กับโค้ดเบสราว 900,000 บรรทัด และบน Ryzen Threadripper 2950X แบบ 16 คอร์ มันก็เสร็จแทบจะทันทีไม่รู้สึกว่าจำเป็นต้องลดเวลาที่ต่ำกว่า 1 วินาทีอยู่แล้วให้ “ต่ำกว่า 1 วินาทีขึ้นอีกนิด”
คุณสมบัติหลักของเครื่องมือแนว
grepรุ่นใหม่จึงไม่ใช่ความเร็ว และควรประเมินกับเปรียบเทียบด้วยวิธีอื่นagมี performance cliff ค่อนข้างใหญ่ และเห็นได้ในบทความบล็อกด้วยอย่างไรก็ตาม workload ของแต่ละคนต่างกัน ดังนั้นในบางกรณีความต่างด้านประสิทธิภาพอาจไม่สำคัญ
900,000 บรรทัดไม่ได้ใหญ่มากนัก และถ้าเป็น query แบบง่าย ๆ เครื่องมือแนว
grepส่วนใหญ่ที่ไม่ได้ทำแบบไร้เดียงสาก็จัดการได้เร็วมากถ้ามองด้วยเกณฑ์เปรียบเทียบอื่น
agแทบจะอยู่ในสภาพประคองชีวิต และดูเหมือนว่าเคยเกือบถูกถอดออกจาก Debian ก่อนจะมีคนช่วยไว้: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=999962บทความบล็อกยังเปรียบเทียบ การรองรับ Unicode ด้วย ซึ่ง
agแทบไม่มีการรองรับ Unicode เลย อาจไม่สำคัญกับทุกคน แต่ก็เพียงพอจะเป็นเกณฑ์เปรียบเทียบที่ไม่ใช่ประสิทธิภาพได้เวลาค้นหาจะกินเวลาพอ ๆ กับเวลาที่โหลดไฟล์จากดิสก์ และความต่างหลังจากนั้นมักมีนัยสำคัญได้ยาก
ถ้าไฟล์อยู่ในแคช เวลาในการย้ายไปมาในไฟล์ซิสเต็มและพิมพ์คำสั่งจะมีอิทธิพลมากกว่าเวลาค้นหาอยู่ดี ดังนั้นความต่างด้านประสิทธิภาพก็มีนัยสำคัญได้ยากเช่นกัน
ในชื่อเรื่องควรมี (2016)
นี่เป็นโพสต์ประกาศเดิม ไม่ใช่ข้อมูลใหม่
“Ripgrep – A new command line search tool” https://news.ycombinator.com/item?id=12564442 (740 points | Sept 23, 2016 | 209 comments) — มีการคุยเรื่องความเร็วด้วย
“Ripgrep is faster (2016)” https://news.ycombinator.com/item?id=17941319 (98 points | Sept 8, 2018 | 40 comments)
ไม่ได้เร็วกว่า
qgrepวิธีทำงานของทั้งสองต่างกันมาก และ
qgrepใช้re2เป็นฐาน แต่ความเร็วได้มาจากการมี ดัชนีใน repository ไฟล์ขนาดใหญ่ การใช้
qgrepกับดัชนีสมเหตุสมผลกว่าการไล่กวาดทุกไฟล์ทุกครั้ง ผมเลยสงสัยว่าทำไมคนถึงลืมตัวเลือกqgrepกันอย่างไรก็ตาม ถ้าต้องการ match หลายบรรทัดใน UTF-8 ผมคิดว่า
ripgrepต้องถอยไปใช้ไลบรารี PCRE2 อีกตัว จึงไม่น่าจะเร็วขนาดนั้นripgrepเป็นเรื่องจริงที่qgrepได้เปรียบเครื่องมือที่ไม่ทำ indexing เพราะมันใช้ indexingแต่ก็ต้องตั้งค่าและดูแลดัชนี ดังนั้น UX จึงไม่เรียบง่ายเท่า “สั่งค้นหาเลย”
เหตุผลที่คนไม่ใช้
qgrepก็คล้ายกับเหตุผลที่ไม่ใช้ripgrepเพราะ “สำหรับผม grep ก็เร็วพอแล้ว”กับเป้าหมายค้นหาขนาดเล็ก หลายครั้งผู้ใช้ไม่รู้สึกถึงความต่างด้านความเร็วระหว่าง
ripgrepกับgrepหรือระหว่างqgrepกับripgrepถ้า
ripgrepค้นหา Linux kernel เสร็จภายใน 100ms การเปลี่ยนไปใช้เครื่องมือแบบ indexing ในการใช้งานโต้ตอบทั่วไปจะคุ้มกับความไม่สะดวกหรือไม่ก็ขึ้นกับสถานการณ์ แต่โดยมากคงไม่ผมเคยคิดถึงไอเดียเพิ่ม indexing ให้
ripgrep: https://github.com/BurntSushi/ripgrep/issues/1497และการค้นหาหลายบรรทัด ไม่จำเป็นต้องใช้ PCRE2 เอนจิน regular expression พื้นฐานก็รองรับ Unicode และแม้จะ build โดยไม่มี PCRE2 ก็ยังคงรองรับการค้นหาหลายบรรทัดอยู่
หลังย้ายจาก
ripgrepไปใช้ ugrep แล้วก็ไม่ได้หันกลับมาอีกความเร็วก็ใกล้เคียงกัน แต่มี fuzzy matching มี TUI ที่ใช้กับ code review ได้ และค้นหาใน PDF หรือไฟล์บีบอัดได้ด้วย
อีกอย่างที่สะดวกคือเลือกใช้ไวยากรณ์การค้นหาแบบ Google ได้
https://ugrep.com
ripgrepแต่ช่วงหลังต้องไปหาugrepเพราะฟีเจอร์ที่ripgrepไม่มีคือ การค้นหาภายในไฟล์ zipค้นหาได้โดยไม่ต้องแตกไฟล์ลงดิสก์
ผมจัดการ corpus ที่ถูกบีบอัดซึ่งมีไฟล์ข้อความเล็ก ๆ หลายล้านไฟล์ และดีที่ไม่ต้องแตกทั้งหมดลงไฟล์ซิสเต็ม เพราะไฟล์ซิสเต็มบางตัวรับมือกับขนาดระดับนี้ลำบาก
ขอบคุณทั้งสองเครื่องมือ และขอบคุณผู้เขียนของแต่ละตัว
grepเริ่มใช้ไวยากรณ์การค้นหาแบบ Google ผมกลัวว่าผลลัพธ์ส่วนใหญ่จะพยายามขายอะไรบางอย่างให้ผมugrepกับripgrepเถียงกันบน Reddit มาหลายปีเช่น https://www.reddit.com/r/programming/comments/120wqvr/ripgre...
ก็แค่เรื่องเครื่องมือโอเพนซอร์ส แต่รู้สึกแปลก ๆ
fzfหรือเปล่าสำหรับผม ดูจะยากที่จะชนะ ความสามารถในการปรับแต่ง และความยืดหยุ่นของ
fzfkiller feature น่าจะเป็น ความเข้ากันได้กับตัวเลือก command line ของ grep เดิม
การไม่ต้องเรียนรู้ชุดตัวเลือกใหม่ทั้งหมดถือว่าดีทีเดียว
สงสัยว่าทำไม
grepถึงไม่ถูกแทนที่หรือปรับปรุงเรื่องนี้เองก็น่าจะค่อนข้างเก่าแล้ว
เช่น ความเฉื่อย, ความเข้ากันได้, การต่อต้านการเปลี่ยนแปลง, ภาวะกลืนไม่เข้าคายไม่ออกของผู้สร้างนวัตกรรม เป็นต้น ไม่ได้พูดในแง่ลบนะ เพราะทั้งหมดนี้ก็ใช้กับผมเหมือนกัน
เรื่องความเข้ากันได้ให้ดู FAQ: https://github.com/BurntSushi/ripgrep/blob/master/FAQ.md#pos...
มันนั่งสบาย เข้ากับสภาพแวดล้อมการทำงานรอบตัวดี และไม่มีเหตุผลพอที่จะเปลี่ยนแล้วต้องปรับทุกอย่างใหม่
อุปมานี้ไปได้แค่ถึงจุดที่ว่า มีเก้าอี้แบบ Razer อยู่ใกล้ ๆ อยู่แล้วและกำลังใช้พาดเสื้อผ้าอยู่
xyzอยู่เสมอ และต้องรับอาร์กิวเมนต์นี้แล้วทำงานแบบนี้อย่างแม่นยำ”ripgrepให้ใช้แล้วถ้าหมายถึงการเปลี่ยนคำสั่ง
grepเองให้เป็นยูทิลิตีตัวอื่น ดูเหมือนว่าจะมีของที่พังมากเกินไปเมื่อเทียบกับคุณค่าที่ได้คนที่ต้องการ
grepที่เร็วขึ้นก็ใช้เครื่องมืออื่น ส่วนคนที่ใช้grepเดิมก็ใช้ต่อไปได้ ดังนั้นตอนนี้ก็ใกล้เคียงกับสภาพที่เหมาะสมแล้วgrepเป็น เครื่องมืออเนกประสงค์ สำหรับค้นหาข้อความในไฟล์ทุกประเภท และฝังอยู่ในมาตรฐาน UNIXโปรแกรมเมอร์บางส่วนใช้มันค้นหาซอร์สโค้ด แต่คนอื่นใช้ค้นหาข้อความที่ไม่เกี่ยวกับซอร์สโค้ดหรือใช้ในสคริปต์ และคาดหวังว่ามันจะต้องไม่แครชเด็ดขาด
ในทางกลับกัน
ripgrepเป็นเครื่องมือเฉพาะทางที่มีแนวทางชัดเจน ออกแบบมาเพื่อค้นหาใน repository ของซอร์สโค้ดเป็นหลักแทบไม่มีช่องให้ทำให้การค้นหาข้อความอเนกประสงค์เร็วขึ้นมากนัก ถ้าใช้
mmap()ก็มีความเสี่ยงแครชกับไฟล์ที่ถูกตัดทอน ถ้าลดความสามารถของ regular expression ก็อาจเร็วขึ้นได้ และอาจทิ้งการรองรับ locale กับชุดอักขระทั้งหมดแล้ว hardcode ให้ใช้แค่ UTF-8/UTF-16 ก็ได้ แต่ไม่ควรทำแบบนั้นลองหาใน Portage แล้วดูเหมือนว่าจะมีเวอร์ชันที่จัดการเอกสารอื่น ๆ อย่าง PDF และ doc ได้ด้วย
https://github.com/phiresky/ripgrep-all