วันที่ดีของ trie-hard: ประหยัดคอมพิวต์ 1%
(blog.cloudflare.com)- Cloudflare เปิดตัว Rust crate แบบโอเพนซอร์ส trie-hard เพื่อลดการใช้ CPU ของพร็อกซีที่ใช้ Rust ชื่อ Pingora บนเครือข่ายที่ประมวลผลคำขอ HTTP เฉลี่ยมากกว่า 60 ล้านครั้งต่อวินาที
- คอขวดอยู่ที่ฟังก์ชัน
clear_internal_headersของpingora-originซึ่งลบเฮดเดอร์สำหรับการกำหนดเส้นทางภายใน การวัดผล และการปรับให้เหมาะสม ออกจากคำขอที่ไม่ได้แคชก่อนออกจาก Cloudflare และทำงานบนเส้นทางคำขอ 35 ล้านครั้งต่อวินาที - การใช้งานเดิมพยายามลบเฮดเดอร์ภายในมากกว่า 100 รายการในทุกคำขอ ใช้เวลาเฉลี่ย 3.65µs และกินเวลา CPU 1.71% ของ
pingora-originทั้งหมด หรือเทียบเท่ากับ CPU core 680 คอร์จาก 40,000 compute-seconds/s - วิธีวนอ่านเฮดเดอร์ของคำขอจริงก่อนทำได้เร็วขึ้น 2.39 เท่า โดยเฉลี่ย 1.53µs แต่การลด CPU โดยรวมในเชิงทฤษฎีทำได้เพียง 0.993% จึงต้องการโครงสร้างค้นหาที่ดีกว่า
- การใช้งาน trie แบบใหม่ลดเวลาเฉลี่ยลงเหลือ 0.93µs และจากการ sampling ที่รันในโปรดักชันตั้งแต่กรกฎาคม 2024 การใช้ CPU จริงของ
clear_internal_headersก็ลดลงเหลือ 0.34%
ต้นทุนที่ Cloudflare ต้องการลด
- เครือข่ายทั่วโลกของ Cloudflare ประมวลผลคำขอ HTTP เฉลี่ย มากกว่า 60 ล้านครั้งต่อวินาที
- การปรับให้เหมาะสมครั้งนี้เริ่มจาก Pingora ซึ่งเป็นแกนหลักของบริการพร็อกซีที่ใช้ Rust และบริการโปรดักชัน
pingora-originที่ทำงานอยู่บนมัน pingora-originรับผิดชอบขั้นตอนสุดท้ายในการส่งคำขอที่ไม่ได้แคชของผู้ใช้ไปยังเซิร์ฟเวอร์ปลายทางจริงหรือ origin- บริการนี้ต้องลบข้อมูลที่ใช้สำหรับการกำหนดเส้นทางภายใน การวัดผล และการปรับให้เหมาะสม ออกจากเฮดเดอร์ ก่อนที่คำขอจะออกจากโครงสร้างพื้นฐานของ Cloudflare
- ทั่วโลก คำขอที่ออกจาก
pingora-originณ เวลาที่เขียนอยู่ที่ 35 ล้านครั้งต่อวินาที
ฟังก์ชันคอขวด: clear_internal_headers
- ฟังก์ชันที่เป็นปัญหาอยู่บนเส้นทางที่ร้อนมากซึ่งทำงานกับทุกคำขอ
// PERF: heavy function: 1.7% CPU time
pub fn clear_internal_headers(request_header: &mut RequestHeader) {
INTERNAL_HEADERS.iter().for_each(|h| {
request_header.remove_header(h);
});
}
- ฟังก์ชันเล็ก ๆ เพียงฟังก์ชันเดียวนี้ใช้เวลา CPU ของ
pingora-originทั้งหมด มากกว่า 1.7% - เวลา CPU รวมของ
pingora-originคือ 40,000 compute-seconds ต่อวินาที ซึ่งมีขนาดเทียบเท่ากับ CPU core ที่ทำงานเต็ม 40,000 คอร์รันบริการนี้อย่างเดียว - ในจำนวนนั้น 1.7% หรือประมาณ 680 CPU core ถูกใช้ไปกับการประเมิน
clear_internal_headersเพียงอย่างเดียว
การวัดค่าพื้นฐานและการปรับปรุงครั้งแรก
- ใช้ Rust crate criterion ในการวัดประสิทธิภาพ
- Criterion รวมผลการรันแบบแยกหลายครั้งเพื่อวัดเวลาโค้ด Rust ในระดับ นาโนวินาที และยังให้ฟีดแบ็กเรื่องประสิทธิภาพที่ดีขึ้นหรือถดถอยตามเวลา
- อินพุต benchmark คือชุดคำขอจำนวนมากที่สังเคราะห์ขึ้นให้เฮดเดอร์ภายในและเฮดเดอร์ที่ไม่ใช่ภายในกระจายอย่างสม่ำเสมอ
- เวลาเฉลี่ยของ
clear_internal_headersแบบเดิมวัดได้ 3.65µs -
กลับทิศทางการค้นหา
- โค้ดเดิมเรียก
request_header.remove_header(h)กับทุกรายการในลิสต์เฮดเดอร์ภายใน ดังนั้นหากมีเฮดเดอร์ภายในมากกว่า 100 รายการ ก็จะประเมินมากกว่า 100 ครั้งต่อคำขอ - จำนวนเฮดเดอร์ของคำขอโดยเฉลี่ยอยู่ที่ 10~30 รายการ ซึ่งน้อยกว่า 100 มาก จึงลดจำนวนครั้งที่ต้องอ่านได้โดยวนผ่านเฮดเดอร์ของคำขอจริงแล้วค้นหาในเซตของเฮดเดอร์ภายใน
- ใน
http::HeaderMapของ Rust ยังไม่มีretainจึงต้องรวบรวมเฮดเดอร์ภายในที่จะลบไว้ในขั้นตอนแยกต่างหากก่อน แล้วค่อยลบ - การเปลี่ยนแปลงนี้เพียงอย่างเดียวลดเวลาเฉลี่ยจาก 3.65µs เหลือ 1.53µs ทำให้เร็วขึ้น 2.39 เท่า
- ในเชิงทฤษฎี การใช้ CPU โดยรวมจะลดจาก 1.71% เหลือ 0.717% โดยส่วนที่ประหยัดได้คือ 0.993%
- โค้ดเดิมเรียก
การสำรวจโครงสร้างข้อมูลค้นหาที่ดีกว่า
- หลังจากเปลี่ยนทิศทางการค้นหาแล้ว ก็สามารถเลือกได้ว่าจะเก็บลิสต์เฮดเดอร์ภายในแบบคงที่ไว้ในโครงสร้างข้อมูลแบบใด
pub fn clear_internal_headers(request_header: &mut RequestHeader) {
let to_remove = request_header
.headers
.keys()
.filter_map(|name| INTERNAL_HEADER_SET.get(name))
.collect::<Vec<_>>();
to_remove.into_iter().for_each(|k| {
request_header.remove_header(k);
});
}
- ความพยายามแรกคือ
std::HashMapแต่ hash ของคีย์สตริงต้องอ่านทุกไบต์ ดังนั้นแม้จะเป็นเวลาคงที่เมื่อเทียบกับขนาดตาราง แต่เมื่อเทียบกับ ความยาวคีย์ L ก็ยังเป็นเวลาเชิงเส้น - เป้าหมายคือหาโครงสร้างข้อมูลที่มีพฤติกรรมการอ่านดีกว่า O(L) เมื่อเทียบกับความยาวคีย์
-
ทางเลือกที่พิจารณา
- เซตแบบเรียงลำดับอย่าง
BTreeSetใช้การค้นหาแบบอิงการเปรียบเทียบ และให้พฤติกรรม O(log(L)) เมื่อเทียบกับความยาวคีย์ แต่เมื่อเทียบกับขนาดของเซตก็ยังใช้เวลาแบบลอการิทึมด้วย - FST ซึ่งเป็นเซตแบบเรียงลำดับที่เร็ว ก็ยังช้ากว่า hashmap มาตรฐานประมาณ 50ns ใน benchmark
- state machine เช่น parser หรือ regular expression รับอินพุตทีละหน่วยและตัดสินใจในแต่ละขั้นว่าจะประเมินต่อหรือไม่ จึงระบุสตริงที่ไม่ตรงกันได้อย่างรวดเร็ว
- เฮดเดอร์ภายในมีเฉลี่ยเพียง 1~2 รายการ ต่อคำขอ จึงเหมาะกับสถานการณ์ที่ส่วนใหญ่เป็นกรณีไม่ตรงกัน
- การใช้งาน
clear_internal_headersด้วย regular expression ใช้เวลานานกว่าโซลูชันที่ใช้ hashmap ประมาณ 2 เท่า แต่ก็ยังเป็นผลลัพธ์ที่เร็วเมื่อพิจารณาว่าไม่ใช่เครื่องมือที่ขึ้นชื่อเรื่องความเร็วดิบ
- เซตแบบเรียงลำดับอย่าง
เหตุผลที่ trie เหมาะสม
- trie เป็นโครงสร้างข้อมูลแบบต้นไม้ที่มักใช้สำหรับการค้นหาคำนำหน้าหรือ autocomplete ในชุดสตริงที่ทราบอยู่แล้ว
- แต่ละโหนดของ trie แทน สตริงย่อย ที่พบในชุดสตริงเริ่มต้น และการเชื่อมต่อระหว่างโหนดแทนอักขระที่สามารถตามหลังคำนำหน้าเฉพาะได้
- เมื่อเริ่มจาก root node จะลดพื้นที่ค้นหาได้ตั้งแต่อักขระแรก ๆ ที่เป็นไปได้ จึงคัดสตริงที่ไม่รวมอยู่ในชุดออกได้อย่างรวดเร็ว
- การอ่าน trie ให้พฤติกรรม O(log(L)) สำหรับกรณีไม่ตรงกัน แต่กรณีตรงกันยังคงเป็น O(L)
- เฮดเดอร์คำขอมากกว่า 90% ไม่ใช่เฮดเดอร์ภายใน จึงเหมาะกับ use case นี้ที่มีกรณีไม่ตรงกันจำนวนมาก
-
ข้อจำกัดของการใช้งาน trie ที่มีอยู่
- benchmark การใช้งาน trie หลายตัวใน crates.io แล้ว แต่ผลลัพธ์ต่ำกว่าที่คาด
- trie ส่วนใหญ่ถูกออกแบบมาสำหรับ use case เช่น autocomplete ที่ตอบสนองต่อเหตุการณ์จากคีย์บอร์ด ไม่ได้ให้ความสำคัญกับการปรับให้เหมาะสมของ hot path ที่มีคำขอหลายสิบล้านครั้งต่อวินาที
- การใช้งานเดิมที่พบว่าเร็วที่สุดคือ radix_trie แต่ยังช้ากว่า hashmap 1µs
- สุดท้ายจึงเขียนการใช้งาน trie ใหม่ให้เหมาะกับ use case นี้
trie-hard และผลลัพธ์ในโปรดักชัน
-
Cloudflare เปิดตัว Rust crate โอเพนซอร์สตัวใหม่ trie-hard
-
trie-hardเก็บความสัมพันธ์ของโหนดไว้ในบิตของจำนวนเต็มแบบ unsigned และเก็บทั้งต้นไม้ไว้ใน บล็อกหน่วยความจำที่ต่อเนื่องกัน เพื่อให้ได้ความเร็ว -
ใน benchmark
trie-hardลดเวลาเฉลี่ยของclear_internal_headersลงเหลือ 0.93µs -
การใช้ CPU ที่คาดการณ์ไว้คือ 0.43% ซึ่งเมื่อเทียบกับ 1.71% เดิมแล้ว ส่งผลให้การใช้คอมพิวต์โดยรวมของ
pingora-originลดลง 1.28% -
ผลการ sampling จริง
trie-hardรันอยู่ในโปรดักชันตั้งแต่กรกฎาคม 2024- การวัดประสิทธิภาพเก็บข้อมูลด้วยการ sampling สถิติของ stack trace ตามเวลา
- ประเมินการใช้ CPU ของฟังก์ชันจากสัดส่วนของ sample ที่มีฟังก์ชันนั้นรวมอยู่
- ผลการ sampling ในโปรดักชันโดยรวมใกล้เคียงกับการคาดการณ์จาก benchmark ในเครื่อง
การใช้งาน sample ของ stack trace ที่มี clear_internal_headersการใช้ CPU จริง การใช้ CPU ที่คาดการณ์ Original 19 / 1111 1.71% n/a Hashmap 9 / 1103 0.82% 0.72% trie-hard 4 / 1171 0.34% 0.43%
ข้อสรุปในมุมมองการดำเนินงาน
- ก่อนจะเขียนโครงสร้างข้อมูลใหม่ ต้องรู้ก่อนว่าโค้ดช้าตรงไหนและช้าแค่ไหน
- การใช้ flame graph, profiling และเครื่องมือ benchmarking ช่วยให้พบการประหยัดที่มีความหมายได้แม้ในฟังก์ชันระดับไมโครวินาที
- การปรับให้เหมาะสมกับงานที่สั้นมากอยู่แล้วอาจดูเหมือนเรื่องเล็กน้อย แต่ในระดับหลายสิบล้านคำขอต่อวินาที การปรับปรุงเล็ก ๆ จะสะสมจนลดการใช้คอมพิวต์โดยรวมได้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ถ้าให้เดาแบบคร่าว ๆ ว่า Cloudflare เก็บและลบ เฮดเดอร์ภายใน อย่างไร ผมคงนึกถึงดิกชันนารีหรือโครงสร้างข้อมูลแยกต่างหาก, เฮดเดอร์เดียวที่บรรจุเมทาดาทาภายในทั้งหมด, วิธีแยกภายใน/ภายนอกด้วยคำนำหน้า หรือคำนำหน้าอย่าง
CFIntคงไม่นึกถึงวิธีที่ถือว่าชื่อเฮดเดอร์ในรายการเฉพาะเป็นเฮดเดอร์ภายใน เลยเกิดคำถามอย่างเช่น ถ้าคนอื่นใช้ชื่อเดียวกันจะเป็นอย่างไร, ถ้าลืมเก็บกวาดจะเกิดอะไรขึ้น, ถ้าโปรแกรมที่รันพร้อมกันมีรายการไม่ตรงกันจะเป็นอย่างไร, ถ้าเฮดเดอร์
Connectionชี้ไปที่เฮดเดอร์ภายในของ Cloudflare จะเป็นอย่างไร, หรือถ้าอัลกอริทึมผลต่างของเซตช้าจะเป็นอย่างไรเว็บก็เต็มไปด้วย สัญญาณ in-band และการตั้งชื่อเฮดเดอร์ที่คลุมเครือจนน่ารำคาญอยู่แล้ว จึงรู้สึกแปลกที่บริษัทระดับ Cloudflare จะใช้กลไกภายในที่ยุ่งยากและเกิดข้อผิดพลาดได้ง่ายแบบนี้
cf-cache-statusรวมถึงการลบเฮดเดอร์ภายในทั้งสองทิศทางที่ edge proxy และทำแบบนั้นกับขาเข้าด้วย
วิธีแบบรายการมีข้อเสีย แต่ก็มีข้อดีมากมาย เวลาแตะถึงข้อบกพร่องที่เป็นไปได้ของแนวทางนี้ มักดูเหมือนละเลยประวัติและความยากลำบากที่เกิดขึ้นในสเกลของ Cloudflare รายการแบบ enum เป็นวิธีที่ง่ายและยืดหยุ่นที่สุด และไม่ต้องมีการตกลงล่วงหน้าเกี่ยวกับโครงสร้างคีย์ของเฮดเดอร์ เมื่อคิดถึงจำนวนทีมของ Cloudflare และความเป็นไปได้ในการซื้อกิจการด้านเทคโนโลยี จุดนี้น่าจะสำคัญมาก
ถ้าเติมไว้ล่วงหน้า ก็เท่ากับผสานไอเดียเหล่านี้เข้าด้วยกัน และทำให้จำนวนเฮดเดอร์ภายในต่อ request คงที่ เมื่อถึงจุดนั้นก็ใช้ linked hash table ที่รักษาลำดับการสร้างไว้ แล้วลบแค่ N รายการแรกออกจากรายการสุดท้ายที่จะส่งกลับไปยังไคลเอนต์ก็พอ
ผมลองคิดชั่วครู่ถึงผลของการแมปอักขระ UTF-8 ด้วย บิตมาสก์ ตอนแรกดูไม่ค่อยฉลาดนัก แต่แล้วก็นึกได้ว่า 32 บิตสามารถเก็บ
a-zกับอักขระพิเศษได้ 6 ตัว และ 64 บิตก็เก็บตัวพิมพ์ใหญ่A-Zกับอักขระพิเศษเพิ่มอีก 6 ตัวได้สำหรับ HTTP header นี่เป็นพื้นที่ที่มากพอ และแค่ mask กับเปรียบเทียบจำนวนเต็มไม่กี่ตัว ก็กลายเป็นอัลกอริทึมจับคู่ที่เร็วมาก การดูว่าอักขระใดตรงกับบิตไหนก็เป็นเพียงการ lookup ตาราง 256 word ครั้งเดียว
สิ่งที่ผู้เขียนตกหล่นไปคือ เทคนิคนี้ในเชิงเทคนิคแล้วคือ Bloom filter เหตุผลที่ของพวกนี้น่าสนใจก็คือ มันเป็นการปรับแต่งจากยุคที่ทรัพยากรคอมพิวต์จำกัดกว่าตอนนี้มาก ในกรณีนี้มาจากปี 1970 แต่เรายังใช้การปรับแต่งเก่าแบบเดียวกันนี้อยู่ตามมุมต่าง ๆ ของโลกจริง
https://en.wikipedia.org/wiki/Bloom_filter
trie-hardกับ Bloom filter มีความต่างกันมาก Bloom filter เป็นแบบความน่าจะเป็นและใช้ hashing เหมาะเมื่อยอมรับ false positive ที่พบได้น้อยเพื่อแลกกับการต้องไม่มี false negative แต่สิ่งที่ต้องการที่นี่ไม่ใช่แบบนั้น ที่นี่ต้องการความถูกต้อง และตัว hashing เองก็คือขั้นตอนที่ต้องเอาชนะให้ได้กลับกัน สิ่งนี้ใกล้เคียงกับเวอร์ชันปรับปรุงหรือดัดแปลงของ อัลกอริทึม Liang ที่ใช้เก็บพจนานุกรมการใส่ยัติภังค์ใน TeX มากกว่า วิทยานิพนธ์พูดถึงอัลกอริทึมของ Bloom ในชื่อ “superimposed coding” ซึ่งทำให้นึกถึงยุคที่หน่วยความจำเป็นทรัพยากรล้ำค่าที่สุดอย่างแรง คุณน่าจะชอบ ^_^
https://tug.org/docs/liang/liang-thesis.pdf
ข้อมูลที่เสนอมาเพื่อพิสูจน์ประโยชน์นั้นจริง ๆ แล้วมี กำลังของการทดสอบ ไม่พอที่จะแสดงให้เห็นเรื่องนั้น ตัวอย่างยังไม่เพียงพอ
ถ้าวิเคราะห์แบบง่ายมากใน R ก็เป็น
prop.test(c(9, 4), c(1103,1171))และในการทดสอบความเท่ากันของสัดส่วนสองค่าพร้อม continuity correction จะได้ค่า p-value 0.222p-value 0.22 ไม่ได้ต่ำกว่าค่า 0.05 วิเศษ และช่วงความเชื่อมั่น 95% ชี้ว่า trie อาจจะแย่กว่าเล็กน้อยจริง ๆ ก็ได้
ดูจากการวิเคราะห์ก่อนหน้าแล้ว trie น่าจะดีกว่า และก็มีหลักฐานอ่อน ๆ อยู่ แต่ควรเก็บตัวอย่างให้มากกว่านี้มาก ๆ เพื่อให้มั่นใจว่าดีกว่าแค่ไหน
สำหรับเป้าหมายที่จะวิเคราะห์ประสิทธิภาพ การอธิบายจำนวนการเปรียบเทียบด้วย Big-O notation ดูไม่ค่อยเข้าที่ การเปรียบเทียบใช้ราว 1 cycle และด้วย instruction-level parallelism กับ SIMD ก็ทำได้หลายครั้งในหนึ่ง cycle
คอขวดจริงและต้นตอของความช้าคือหน่วยความจำ การเข้าถึงหน่วยความจำอาจใช้หลายพัน cycle และถ้าต้องมี TLB walk หรือ OS interrupt ก็อาจขึ้นไปถึงหลายหมื่นหรือหลายแสน cycle ได้ ถ้าอยากใช้ Big-O ก็ควรใช้เพื่อประมาณ จำนวน cache miss
ถ้าเป็นผมคงใช้ฟังก์ชัน perfect hash แบบปรับแต่งเองกับทริก
popcountของ Phil Bagwell น่าจะเร็วกว่าทางแก้อื่น ๆ ที่ต้อง lookup หน่วยความจำหลายครั้งCPU เร็ว ส่วนหน่วยความจำช้า
ผมไม่ได้เชี่ยวชาญเรื่องการปรับแต่งโครงสร้างข้อมูลมากนัก แต่โดยเฉพาะเมื่อคำนึงว่าตารางที่เป็นเป้าหมายในการค้นหาเป็นแบบคงที่ ก็แปลกใจที่พวกเขาตัด ตารางแฮช ออกเร็วเกินไป ยากจะเชื่อว่าตารางแฮชที่ปรับแต่งเป็นพิเศษจะไม่เร็วกว่า implementation แบบ trie ของพวกเขา
สำหรับการใช้งานนี้ นั่นคือข้อได้เปรียบที่แฮชตามไม่ทัน เทคนิคที่เหลือคือการลด constant factor ของ trie ให้มากพอจนข้อได้เปรียบตั้งต้นนี้สะท้อนออกมาในประสิทธิภาพจริง
std::collections::HashMap: https://github.com/rust-lang/rustc-hashถ้ารายการคงที่มีประมาณ 100 รายการ o1hash ก็น่าจะใช้ได้: https://github.com/rurban/smhasher/blob/master/o1hash.h
บทความนี้สร้างชุด
to_removeด้วยโครงสร้างข้อมูลเจ๋ง ๆ แล้ววนผ่านมันเพื่อลบออกจาก header map หลักการเรียก
remove_headerดูเหมือนจะเป็นโค้ดนี้: https://docs.rs/pingora-http/0.3.0/src/pingora_http/lib.rs.html#576ฟังก์ชันนี้เรียก
.remove()บนโครงสร้างข้อมูลอีกสองตัว และทั้งคู่สุดท้ายก็ไหลลงไปที่โค้ดขนาดใหญ่นี้: https://docs.rs/http/latest/src/http/header/map.rs.html#1550ในที่สุดก็มีบล็อกโพสต์ที่พูดถึง trie แล้ว ปัญหา trie ใน LeetCode ไม่สูญเปล่าสินะ ;)
ถึงอย่างนั้นก็ไม่ใช่เครื่องมือที่หยิบมาใช้บ่อยแน่นอน
เป็น parser user agent สำหรับ Go: https://github.com/medama-io/go-useragent
ปกติปัญหาแบบนี้พึ่งพาการ parse regex จำนวนมาก เลยดีที่ได้ลองแนวทางที่ใหม่กว่านั้น
ถ้าชุดรายการที่ต้อง match เป็นแบบคงที่ ก็สงสัยว่าพวกเขาลอง perfect hash table แล้วหรือยัง หลังจากคำนวณเลขคณิตไม่กี่ครั้งก็น่าจะลดเหลือการเปรียบเทียบสตริงครั้งเดียว น่าสนใจว่าจะเทียบกับ trie เป็นอย่างไร
ในทางทฤษฎี ถ้าใช้ regex มันจะ match ด้วย state machine ดังนั้นกรณีแย่สุดควรได้ประสิทธิภาพ O(k) คล้าย trie แต่เท่าที่ผมเข้าใจ ไลบรารี regex มักใช้ backtracking มากกว่าสร้าง state machine จริง ๆ ประสิทธิภาพจึงไม่ใช่ O(k) อีกต่อไป
ก็น่าแปลกใจที่พวกเขาหาไลบรารี regex ที่ทำงานบน state machine และมีประสิทธิภาพดีอยู่แล้วไม่เจอ มันน่าจะมีประสิทธิภาพใกล้เคียง trie แต่ในทางปฏิบัติ ปัจจัยอย่างรูปแบบการเข้าถึงหน่วยความจำหรือประสิทธิภาพของคำสั่งเลขคณิตบางอย่างมีผลลึกกว่า จึงเดายากอยู่ดี
ได้ลองใช้ Bloom filter ขนาดเล็กมากหรือเปล่า? ถ้าประมวลผล header key อย่างรวดเร็วคล้าย convolution แล้วเทียบกับ Bloom filter ก็น่าจะหลีกเลี่ยงการเดิน trie ได้เกือบทั้งหมดในกรณีส่วนใหญ่ ด้วยต้นทุนเพียงไม่กี่ cycle
ดูเหมือนจะมีตัวเลือกอย่าง SIMD, คำสั่ง CRC ในตัว, ขนาด Bloom filter 256 บิตด้วย
แนวทางที่ง่ายกว่านั้นก็อาจคุ้มค่า การวิเคราะห์ความถี่ hit/miss ของ node ใน trie อาจพบตำแหน่งอักขระบางตำแหน่งที่มีอัตราล้มเหลวสูงกว่าอักขระตัวแรก ถ้าตรวจตำแหน่งพิเศษเหล่านั้นก่อนก็อาจเร็วขึ้นได้ แน่นอนว่าต้องอาศัยสมมติฐานว่า header data มีรูปแบบค่อนข้างสม่ำเสมอโดยธรรมชาติ
ความเห็นของผมคือแบบนี้ อย่างแรก นี่เป็นงานที่คุ้มทำไหม? ดูเหมือนจะประหยัดได้ประมาณ 500 CPU cores แต่ไม่รู้ว่านี่หมายถึงคอร์จริง ๆ หรือรวมคอร์แบบ hyper-threaded ด้วย
ผมไม่รู้ต้นทุนของ Cloudflare แต่ก็น่าจะเทียบได้กับเซิร์ฟเวอร์จำนวนหลักหน่วย และเงินที่ประหยัดได้ก็คงอยู่ระดับหลายหมื่นดอลลาร์ต่อปี ไม่ใช่จำนวนที่มองข้ามได้ แต่ก็ไม่แน่ใจว่าจะคาดหวัง ROI เป็นบวกเมื่อเทียบกับการลงทุนด้านวิศวกรรมได้หรือไม่
อย่างที่สอง ถ้าจะลงรายละเอียดถึงขนาดนี้ ผมก็สงสัยว่าได้พิจารณาแนวทางใส่ฟิลเตอร์ในขั้นตอน deserialization เพื่อไม่ให้ header ถูกสร้างขึ้นมาตั้งแต่แรกหรือเปล่า
ประหยัดไฟต่อเนื่อง และลดการปล่อยคาร์บอนด้วย
ผมชอบเห็นบริษัทพยายามทำอะไรสักอย่างให้ เร็วขึ้น 1% มากกว่าทำเรื่องโง่ ๆ อย่างเอา AI ไปวิเคราะห์ header เพื่อพยายามขายรองเท้าแตะ
มูลค่าของมันอาจเป็นล้านดอลลาร์ก็ได้