2 คะแนน โดย GN⁺ 2026-01-08 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ทดสอบข้อถกเถียงล่าสุดเรื่อง ความไม่สมดุลระหว่างประสิทธิภาพ 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 ความคิดเห็น

 
GN⁺ 2026-01-08
ความเห็นจาก 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 ต่ำกว่าความเป็นจริงมาก
    • สงสัยว่าข้อจำกัดนี้มาจากไหน — มาจาก โครงสร้างบัส ระหว่างคอร์กับแคช หรือระหว่างแคชกับ memory controller หรือไม่
    • สงสัยว่าทำไม Apple M series ถึงมีแบนด์วิดท์ต่อคอร์สูงกว่า x86 ถึง 3 เท่า
    • บนชิปสมัยใหม่ แค่ CPU อย่างเดียวทำให้ใช้ memory bandwidth ได้เต็มยาก และต้องอาศัย iGPU
      เพราะ iGPU เข้าถึง unified memory ได้
      ดังนั้นงานอย่างการคัดลอกหน่วยความจำขนาดใหญ่, การ parsing แบบขนาน, การบีบอัด/คลายบีบอัด การใช้ iGPU เป็น blitter จึงได้เปรียบในเชิงเทคนิค
      อย่างไรก็ตาม การ ‘ข้าม’ ที่พูดถึงใน zero-copy format นั้นเกิดขึ้นในระดับ cache line
    • Samsung NVMe SSD โฆษณาความเร็วอ่าน 14GB/s แต่ถ้า CPU คอร์เดี่ยวได้แค่ 6GB/s ความสัมพันธ์ระหว่างตัวเลขเหล่านี้ก็น่าสนใจ
  • ดูเหมือนผู้เขียนต้นฉบับจะตีความผลลัพธ์ของคำสั่ง 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
    • ถ้าการทดสอบนี้รันบนคอร์เดี่ยวจริง ข้ออ้างเรื่อง “ขีดจำกัด 6GB/s” ข้างต้นก็ถือว่า ถูกหักล้างจากการทดลอง
  • คอขวดด้านประสิทธิภาพไม่ใช่ปัจจัยเดี่ยวแบบ “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 ที่แท้จริง ก็ยังต้องใช้แนวทางอื่นไม่ว่ามันจะเป็นดิสก์จานหมุนหรือไม่ก็ตาม
    • บน Linux ก็ทำอะไรคล้าย ๆ กันได้
      ที่จริงแล้วคำขอหน่วยความจำส่วนใหญ่ก็เกิดผ่าน mmap()
      เพียงแต่เคอร์เนลคาดเดารูปแบบการเข้าถึงได้ยาก จึงอาจช้ากว่า read/write
  • ในสภาพแวดล้อมคลาวด์ ประสิทธิภาพอาจกลายเป็น เครื่องมือสำหรับปรับราคาแพ็กเกจ ได้ด้วย
    ฮาร์ดแวร์พัฒนาไปอย่างน่าทึ่ง แต่ซอฟต์แวร์บางตัวกลับรู้สึกช้าลงกว่าเดิม โดยเฉพาะ Windows หรือแอปส่งข้อความ

    • ในความเป็นจริง ประสิทธิภาพของ cloud instance ช้ากว่า M1 MacBook ถึง 5 เท่า แต่กลับแพงกว่ามาก
      ไม่มีประสิทธิภาพสำหรับใช้เป็น remote workstation ของนักพัฒนา
    • สาเหตุที่แอป GUI ส่วนใหญ่ยังช้าก็ยังคงเป็นเพราะ รอ I/O
      Telegram หรือ FB Messenger เร็ว แต่ Teams หรือ Skype ไม่เป็นแบบนั้น
    • จอ CRT แสดงผลข้อมูลได้เร็วกว่า
      LCD บางรุ่นมี latency ถึง 500ms
  • ตอน NVMe SSD ออกมาใหม่ ๆ ฉันเคยล้อเล่นว่า “ตอนนี้ก็เหมือนมี RAM 2TB แล้ว”
    แต่ทุกวันนี้เซิร์ฟเวอร์ GPU บางเครื่องติดตั้ง RAM 2TB จริง ๆ — เป็นงานวิศวกรรมที่น่าทึ่ง

    • ฉันเคยเห็นเซิร์ฟเวอร์ Epyc มือสองที่จัดสเปก 2TB DDR4 RAM ในราคา 5,000 ดอลลาร์
      ยังเสียดายที่ตอนนั้นไม่ได้ซื้อไว้
  • จากประสบการณ์ในการปรับแต่งฐานข้อมูล OLAP สำหรับสภาพแวดล้อมที่มี concurrency สูง คอขวดส่วนใหญ่มักเป็น ความเร็วหน่วยความจำ

  • เดิมทีคอขวด I/O ไม่ได้หมายถึง การอ่านแบบลำดับ แต่เกี่ยวข้องกับเวลา seek มากกว่า
    เข้าใจประเด็นของบทความ แต่ก็อยากชี้จุดนี้ไว้

    • ด้วยเทคโนโลยีใหม่อย่าง CXL/PCIe ตอนนี้แม้แต่ RAM และ memory controller ก็อาจถูกมองเป็นอุปกรณ์ I/O ชนิดหนึ่งได้
    • ตอนเรียนวิชาฐานข้อมูลเมื่อก่อน ประสิทธิภาพ I/O วัดจากเวลา seek ของฮาร์ดดิสก์
      เพราะความเร็วการอ่านแบบลำดับนั้นปรับปรุงด้วยโค้ดไม่ได้ ดังนั้นหัวใจสำคัญจึงอยู่ที่ การปรับแต่งการเข้าถึงแบบไม่ต่อเนื่อง