- ทดสอบข้อถกเถียงล่าสุดเรื่อง ความไม่สมดุลระหว่างประสิทธิภาพ I/O กับความเร็วในการประมวลผลของ CPU ด้วยการทดลอง และแสดงให้เห็นว่าในทางปฏิบัติ CPU ยังคงเป็นข้อจำกัดหลัก
- ความเร็วในการอ่านแบบลำดับ อยู่ที่ 1.6GB/s เมื่อแคชเย็น และ 12.8GB/s เมื่อแคชอุ่น แต่การนับความถี่ของคำบนเธรดเดี่ยวทำได้เพียงประมาณ 278MB/s
- โครงสร้าง branch ในโค้ดขัดขวางการทำ vectorization และแม้จะปรับแต่งการแปลงเป็นตัวพิมพ์เล็กแบบง่าย ๆ ก็ยังดีขึ้นได้เพียงราว 330MB/s
- แม้แต่คำสั่ง
wc -w ก็ทำได้เพียง 245MB/s เท่านั้น จึงยืนยันได้ว่า การประมวลผลและการจัดการ branch ของ CPU เป็นคอขวดมากกว่าดิสก์
- การทำ vectorization แบบแมนนวลด้วย AVX2 ดันขึ้นไปได้ถึง 1.45GB/s แต่ก็ยังอยู่ที่เพียง ประมาณ 11% ของความเร็วการอ่านแบบลำดับ จึงพิสูจน์ว่า CPU ไม่ใช่ I/O คือคอขวด
เปรียบเทียบความเร็ว I/O กับประสิทธิภาพ CPU
- ตามข้ออ้างของ Ben Hoyt มีการทดลองว่าการ เพิ่มขึ้นของความเร็วในการอ่านแบบลำดับ ในยุคปัจจุบันแซงหน้าภาวะชะงักงันของความเร็ว CPU ไปแล้วหรือไม่
- เมื่อวัดด้วยวิธีเดียวกัน ได้ค่า 1.6GB/s สำหรับแคชเย็น และ 12.8GB/s สำหรับแคชอุ่น
- อย่างไรก็ตาม เมื่อนับความถี่ของคำบนเธรดเดี่ยว กลับทำได้เพียง 278MB/s
- แม้ในสถานะแคชอุ่น ก็ยังมีความเร็วเพียงประมาณ 1/5 ของความเร็วในการอ่านจากดิสก์
การทดลองนับความถี่ของคำด้วยภาษา C
- คอมไพล์
optimized.c ด้วย GCC 12 โดยใช้ตัวเลือก -O3 -march=native แล้วรันกับไฟล์อินพุตขนาด 425MB
- ผลลัพธ์: ใช้เวลา 1.525 วินาที ความเร็วในการประมวลผล 278MB/s
- การมี branch หลายจุดและการออกจากการทำงานก่อนในโค้ด ขัดขวาง การเพิ่มประสิทธิภาพแบบ vectorization ของคอมไพเลอร์
- หลังย้ายลอจิกการแปลงเป็นตัวพิมพ์เล็กออกไปนอกลูป ความเร็วเพิ่มเป็น 330MB/s
- เมื่อใช้ Clang การทำ vectorization ทำได้ดีกว่า
เปรียบเทียบการนับคำแบบง่าย (wc -w)
- รันคำสั่ง
wc -w ที่นับเพียงจำนวนคำแทนการนับความถี่
- ผลลัพธ์: 245.2MB/s ซึ่งช้ากว่าที่คาดไว้
wc ต้องจัดการอักขระช่องว่างหลายชนิด เช่น ' ', '\n', '\t' รวมถึงอักขระตาม locale
- จึงมีภาระการคำนวณมากกว่าโค้ดที่แยกด้วยช่องว่างธรรมดาเพียงอย่างเดียว
ความพยายามทำ vectorization ด้วย AVX2
- ใช้ความสามารถของ CPU รุ่นใหม่เพื่อทำ vectorization ด้วยชุดคำสั่ง AVX2
- ใช้รีจิสเตอร์ 256 บิต และจัดแนวข้อมูลแบบ 32 บิต
- ใช้คำสั่ง
VPCMPEQB เพื่อเปรียบเทียบอักขระช่องว่าง
- ใช้ บิตมาสก์ (PMOVMSKB) และคำสั่ง Find First Set(ffs) เพื่อตรวจจับขอบเขตของคำ
- ได้แนวคิดมาจากการติดตั้งใช้งาน
strlen ใน Cosmopolitan libc
ผลลัพธ์ด้านประสิทธิภาพและข้อสรุป
- โค้ด vectorization แบบแมนนวล (
wc-avx2) ทำความเร็วได้ถึง 1.45GB/s
- ตรวจสอบแล้วว่าได้ผลลัพธ์เดียวกับ
wc -w (82,113,300 คำ)
- แม้ในสภาวะแคชเย็น เวลาประมวลผลใน user mode ก็ยังเป็นส่วนที่ครองสัดส่วนหลัก
- ยืนยันได้ว่าคอขวดอยู่ที่การคำนวณของ CPU มากกว่า Disk I/O
- โดยรวมแล้ว ความเร็วของดิสก์นั้นเร็วเพียงพออยู่แล้ว แต่ การจัดการ branch และการคำนวณแฮชรวมถึงงานของ CPU ยังเป็นปัจจัยจำกัด
- โค้ดและผลการทดลองถูกเผยแพร่บน GitHub (
haampie/wc-avx2)
1 ความคิดเห็น
ความเห็นจาก Hacker News
คิดว่าขีดจำกัดด้าน ประสิทธิภาพ ของ CPU ยุคใหม่ถูกกำหนดโดยปริมาณข้อมูลที่คอร์เดี่ยวประมวลผลได้ หรือก็คือความเร็วของ
memcpy()คอร์ x86 ส่วนใหญ่ทำได้ราว 6GB/s ส่วน Apple M series อยู่ที่ราว 20GB/s
ตัวเลขอย่าง ‘200GB/s’ ที่ใช้ในการโฆษณาเป็นเพียงแบนด์วิดท์รวมของทุกคอร์เท่านั้น โดยคอร์เดี่ยวยังคงอยู่แถว ๆ 6GB/s
ดังนั้นต่อให้เขียน parser ได้สมบูรณ์แบบก็ไม่อาจทะลุขีดจำกัดนี้ได้
แต่ถ้าใช้ zero-copy format CPU จะข้ามข้อมูลที่ไม่จำเป็นได้ ทำให้ในทางทฤษฎีสามารถ ‘เกิน’ 6GB/s ได้
ฟอร์แมต Lite³ ที่ฉันกำลังพัฒนาใช้หลักการนี้และทำความเร็วได้สูงสุดมากกว่า simdjson ถึง 120 เท่า
ตัวอย่างเช่น Zen 1 แสดงผลได้ 25GB/s บนคอร์เดี่ยว(ลิงก์อ้างอิง)
ตามผล microbenchmark ที่ฉันเขียน Zen 2 ทำได้ 17GB/s เมื่อไม่ใช้ AVX และขึ้นไปถึง 35GB/s เมื่อใช้ non-temporal AVX
บน Apple M3 Max วัดได้สูงถึง 125GB/s ด้วย non-temporal NEON
ดังนั้นตัวเลข x86 6GB/s และ Apple 20GB/s ต่ำกว่าความเป็นจริงมาก
เพราะ iGPU เข้าถึง unified memory ได้
ดังนั้นงานอย่างการคัดลอกหน่วยความจำขนาดใหญ่, การ parsing แบบขนาน, การบีบอัด/คลายบีบอัด การใช้ iGPU เป็น blitter จึงได้เปรียบในเชิงเทคนิค
อย่างไรก็ตาม การ ‘ข้าม’ ที่พูดถึงใน zero-copy format นั้นเกิดขึ้นในระดับ cache line
ดูเหมือนผู้เขียนต้นฉบับจะตีความผลลัพธ์ของคำสั่ง
timeผิดเวลา
systemคือเวลา CPU ที่เคอร์เนลใช้แทนโปรเซสในตัวอย่าง ถ้า
realเท่ากับ 0.395s,userเท่ากับ 0.196s และsysเท่ากับ 0.117s แปลว่า CPU ทำงานจริงรวมเพียง 313ms และอีก 82ms ที่เหลือเป็นช่วง idleกล่าวคือมันทำงานเร็วกว่า disk subsystem จริง แต่ไม่ได้ทิ้งห่างมาก
อีกทั้งเส้นทาง I/O ก็อยู่ในภาวะ CPU-bound — ต่อให้ดิสก์และโค้ดเร็วแบบไม่มีที่สิ้นสุด ก็ยังต้องใช้ 117ms เพื่อรันโค้ด I/O ของเคอร์เนล
ฉันคือผู้เขียนโพสต์ต้นฉบับ มีภาคต่อคือ: I/O is no longer the bottleneck, part 2
บทวิเคราะห์ ที่พูดถึงเทคนิคการปรับแต่งต่าง ๆ ที่ผู้เข้าแข่งขันใช้ก็น่าสนใจ
แนวทางที่ใช้แตกต่างกันไปตามความซับซ้อนของปัญหาและจำนวนประเภทของ whitespace
คอขวดด้านประสิทธิภาพไม่ใช่ปัจจัยเดี่ยวแบบ “CPU หรือ I/O” เสมอไป แต่คือ ทรัพยากรที่อิ่มตัวก่อน ใน workload จริง
จะเป็น CPU, memory bandwidth, cache, disk, network, lock หรือ latency ก็ได้
ดังนั้นต้องวัดผล, พิสูจน์ด้วย profiling และวัดใหม่หลังเปลี่ยนแปลง
ปัญหาไม่ใช่ CPU หรือ I/O แต่เป็นสมดุลระหว่าง latency กับ throughput
ซอฟต์แวร์ส่วนใหญ่ช้าเพราะมองข้าม latency
ถ้าจัดวางข้อมูลในหน่วยความจำแบบเชิงเส้น หรือใช้ batch processing และการประมวลผลแบบขนาน ก็จะเร็วขึ้นมาก
ลองจินตนาการถึงสถาปัตยกรรมที่ประกอบด้วยเพียง CPU ↔ cache ↔ non-volatile storage
ถ้า
mmap()มีคุณลักษณะด้านประสิทธิภาพเหมือนmalloc()เราก็อาจระบุหน่วยความจำของโปรแกรมด้วยชื่อไฟล์ แล้วปล่อยให้ OS ดูแล persistence ได้ทุกวันนี้ซอฟต์แวร์จำนวนมากยังติดอยู่กับ ข้อจำกัดจากยุคฮาร์ดดิสก์
fsync()ก็ยังช้าอยู่ดีถ้าต้องการ persistence ที่แท้จริง ก็ยังต้องใช้แนวทางอื่นไม่ว่ามันจะเป็นดิสก์จานหมุนหรือไม่ก็ตาม
ที่จริงแล้วคำขอหน่วยความจำส่วนใหญ่ก็เกิดผ่าน
mmap()เพียงแต่เคอร์เนลคาดเดารูปแบบการเข้าถึงได้ยาก จึงอาจช้ากว่า
read/writeในสภาพแวดล้อมคลาวด์ ประสิทธิภาพอาจกลายเป็น เครื่องมือสำหรับปรับราคาแพ็กเกจ ได้ด้วย
ฮาร์ดแวร์พัฒนาไปอย่างน่าทึ่ง แต่ซอฟต์แวร์บางตัวกลับรู้สึกช้าลงกว่าเดิม โดยเฉพาะ Windows หรือแอปส่งข้อความ
ไม่มีประสิทธิภาพสำหรับใช้เป็น remote workstation ของนักพัฒนา
Telegram หรือ FB Messenger เร็ว แต่ Teams หรือ Skype ไม่เป็นแบบนั้น
LCD บางรุ่นมี latency ถึง 500ms
ตอน NVMe SSD ออกมาใหม่ ๆ ฉันเคยล้อเล่นว่า “ตอนนี้ก็เหมือนมี RAM 2TB แล้ว”
แต่ทุกวันนี้เซิร์ฟเวอร์ GPU บางเครื่องติดตั้ง RAM 2TB จริง ๆ — เป็นงานวิศวกรรมที่น่าทึ่ง
ยังเสียดายที่ตอนนั้นไม่ได้ซื้อไว้
จากประสบการณ์ในการปรับแต่งฐานข้อมูล OLAP สำหรับสภาพแวดล้อมที่มี concurrency สูง คอขวดส่วนใหญ่มักเป็น ความเร็วหน่วยความจำ
เดิมทีคอขวด I/O ไม่ได้หมายถึง การอ่านแบบลำดับ แต่เกี่ยวข้องกับเวลา seek มากกว่า
เข้าใจประเด็นของบทความ แต่ก็อยากชี้จุดนี้ไว้
เพราะความเร็วการอ่านแบบลำดับนั้นปรับปรุงด้วยโค้ดไม่ได้ ดังนั้นหัวใจสำคัญจึงอยู่ที่ การปรับแต่งการเข้าถึงแบบไม่ต่อเนื่อง