30 คะแนน โดย GN⁺ 2026-01-15 | 5 ความคิดเห็น | แชร์ทาง WhatsApp
  • ThreadMXBean.getCurrentThreadUserTime() ของ OpenJDK ถูกเปลี่ยนจากการ พาร์สไฟล์ /proc มาเป็นการเรียก clock_gettime() และทำให้ประสิทธิภาพดีขึ้นได้สูงสุด 400 เท่า
  • อิมพลีเมนเทชันเดิมต้องผ่าน เส้นทาง I/O ที่ซับซ้อน โดยเปิด อ่าน และพาร์สไฟล์ /proc/self/task/<tid>/stat
  • อิมพลีเมนเทชันใหม่ใช้ การเข้ารหัสบิตของ clockid_t ใน Linux kernel โดยปรับบิตล่างของ ID ที่ได้จาก pthread_getcpuclockid() เพื่อ ดึงเฉพาะ user time โดยตรง
  • ผลเบนช์มาร์กแสดงว่าเวลาเฉลี่ยต่อการเรียกลดลงจาก 11μs → 279ns และหลังเพิ่ม kernel fast-path แล้วยัง ดีขึ้นอีกราว 13%
  • เป็นกรณีศึกษาที่แสดงให้เห็นว่าสามารถทำ optimization ได้ด้วย ความเข้าใจ ABI ภายในของ Linux ที่ก้าวข้ามข้อจำกัดของ POSIX

ปัญหาของอิมพลีเมนเทชันเดิม

  • getCurrentThreadUserTime() จะเปิดไฟล์ /proc/self/task/<tid>/stat แล้วพาร์ส ฟิลด์ที่ 13 และ 14 เพื่อคำนวณค่า CPU user time
    • ต้องมีหลายขั้นตอน เช่น สร้าง path ของไฟล์ เปิดไฟล์ อ่านบัฟเฟอร์ พาร์สสตริง และเรียก sscanf()
    • เนื่องจากชื่อคำสั่งอาจมีวงเล็บอยู่ด้วย จึงมี logic ที่ซับซ้อนในการหา ) ตัวสุดท้ายด้วย strrchr()
  • ในทางกลับกัน getCurrentThreadCpuTime() ใช้เพียงการเรียก clock_gettime(CLOCK_THREAD_CPUTIME_ID) ครั้งเดียว
  • ตามรายงานบั๊กในปี 2018 (JDK-8210452) ความเร็วของสองเมธอดนี้ต่างกันถึง 30~400 เท่า

เปรียบเทียบเส้นทางการเข้าถึง /proc กับเส้นทาง clock_gettime()

  • วิธีแบบ /proc มีทั้ง open(), read(), sscanf(), close() และยังรวมถึง การสร้างสตริงภายในเคอร์เนลหลายส่วน
  • วิธีแบบ clock_gettime() เป็น system call เพียงครั้งเดียว ที่อ่านค่าจากโครงสร้าง sched_entity โดยตรง
  • เมื่อมีโหลดแบบขนาน การเข้าถึง /proc จะยิ่งช้าลงจาก การแข่งขันแย่ง kernel lock

วิธีอิมพลีเมนเทชันใหม่

  • มาตรฐาน POSIX กำหนดว่า CLOCK_THREAD_CPUTIME_ID ต้องคืนค่า user+system time
  • Linux kernel เข้ารหัส ชนิดของนาฬิกา ไว้ในบิตล่างของ clockid_t
    • 00=PROF, 01=VIRT(เฉพาะ user), 10=SCHED(user+system)
  • หากเปลี่ยนบิตล่างของ clockid ที่ได้จาก pthread_getcpuclockid() ให้เป็น 01 ก็จะเปลี่ยนเป็น นาฬิกาที่ให้เฉพาะ user time ได้
  • โค้ดใหม่จึงตัด file I/O และการพาร์สออกไป แล้วคืนค่า user time ด้วยการเรียก clock_gettime() อย่างเดียว

ผลการวัดประสิทธิภาพ

  • ก่อนแก้ เวลาเฉลี่ยต่อการเรียกคือ 11.186μs, หลังแก้เหลือ 0.279μs หรือดีขึ้นราว 40 เท่า
    • วัดในสภาพแวดล้อม 16 เธรด และสอดคล้องกับช่วง 30~400 เท่าที่เคยมีรายงานไว้
  • จาก CPU profile จะเห็นว่า system call ที่เกี่ยวกับการเปิด-ปิดไฟล์หายไป และเหลือเพียงการเรียก clock_gettime() ครั้งเดียว

การเพิ่มประสิทธิภาพเพิ่มเติมด้วย kernel fast-path

  • เคอร์เนลมี fast-path สำหรับกรณีที่ใน clockid มีการเข้ารหัส PID=0 ซึ่งจะเข้าถึงเธรดปัจจุบันได้ทันที
  • หาก JVM สร้าง clockid ขึ้นเองโดยตรงแทน pthread_getcpuclockid() และใส่ PID=0 ก็จะ ข้ามการค้นหา radix tree ได้
  • เมื่อใช้ clockid ที่ประกอบเอง เวลาเฉลี่ยลดจาก 81.7ns → 70.8ns หรือดีขึ้นเพิ่มอีกราว 13%
  • อย่างไรก็ตาม วิธีนี้พึ่งพารายละเอียดภายในของเคอร์เนล เช่น ขนาดของ clockid_t จึงมีความเสี่ยงด้าน ความอ่านง่ายและความเข้ากันได้

บทสรุปและข้อคิด

  • การลบโค้ด 40 บรรทัดช่วย ลบช่องว่างด้านประสิทธิภาพ 400 เท่า ได้ โดยไม่ต้องเพิ่มฟีเจอร์ใหม่ในเคอร์เนล แต่อาศัยเพียง การใช้รายละเอียดของ ABI ที่มีอยู่เดิม
  • ตอกย้ำ คุณค่าของการอ่านซอร์สโค้ดเคอร์เนล: POSIX ให้การรับประกันเรื่อง portability แต่โค้ดของเคอร์เนลแสดงให้เห็น ขอบเขตของสิ่งที่เป็นไปได้
  • ความสำคัญของการทบทวนสมมติฐานเดิม: การพาร์ส /proc เคยสมเหตุสมผลในอดีต แต่ปัจจุบันไม่มีประสิทธิภาพแล้ว
  • การเปลี่ยนแปลงนี้จะถูกรวมใน JDK 26 (มีกำหนดออกในเดือนมีนาคม 2026) และจะช่วยเพิ่มประสิทธิภาพโดยอัตโนมัติเมื่อเรียก ThreadMXBean.getCurrentThreadUserTime()

5 ความคิดเห็น

 
crawler 2026-01-15

น่าทึ่งมาก

ถ้าเร็วขึ้น 2 เท่า อาจเป็นเพราะทำอะไรได้ฉลาดขึ้น แต่ถ้าเร็วขึ้น 100 เท่า ก็แค่หยุดทำเรื่องโง่ ๆ เท่านั้น

ผมคิดว่ามันก็ไม่ใช่คำพูดที่ผิดไปเสียทีเดียว แต่ในกรณีที่เกี่ยวพันกับเคอร์เนล ผมคิดว่าแค่จะสังเกตให้รู้ว่ามันช้าก็คงยากมากแล้ว

 
[ความคิดเห็นนี้ถูกซ่อน]
 
princox 2026-01-19

เรื่องแบบนี้จะค้นพบในโปรเจกต์ได้อย่างไรบ้างครับ? ดูแล้วน่าจะยากที่จะรู้ได้แค่เพราะรัน AI..

พอได้เห็นกรณีแบบนี้แล้ว ก็ทำให้ผมคิดว่าอยากเรียนรู้และอยากลองมีประสบการณ์แบบนี้ด้วยตัวเองให้ได้ครับ

 
aobamisaki 2026-01-15

จริง ๆ แล้วแค่ยกเครื่องโค้ดใหม่ทั้งหมดแล้วยังจะให้ดีขึ้นสัก 2~3 เท่าก็เป็นเรื่องยากอยู่แล้ว แต่นี่แค่เปลี่ยนไม่กี่บรรทัดแล้วดีขึ้นได้สูงสุดถึง 400 เท่า น่าทึ่งจริง ๆ ครับ

 
GN⁺ 2026-01-15
ความคิดเห็นจาก Hacker News
  • ผมคือผู้เขียนเอง หลังจากบทความก่อนหน้าเกี่ยวกับบั๊กในเคอร์เนล ผมได้ไปดูวิธีที่ JVM รายงานกิจกรรมของเธรดด้วยตัวมันเอง
    แล้วก็พบว่าคำถามว่า “เวลาใช้งาน CPU ของเธรดนี้เท่าไร?” เป็นการคำนวณที่ แพงมากเกินคาด
    • ถ้าจะพูดถึงการวัดในระดับนาโนวินาที ก็ต้องเข้าใจ เสถียรภาพและความแม่นยำของนาฬิกา ให้ดีมาก
      ถ้าไม่มีมาตรฐานอ้างอิงระดับนาฬิกาอะตอม ก็รู้สึกว่ายากที่จะอ้างตัวเลขแบบสัมบูรณ์ได้
    • สงสัยว่าได้ดูหรือยังว่าทำไมการกระจายตัวถึง กว้างออกไปหลายลำดับขั้น ซึ่งในตัวมันเองก็น่าสนใจมาก
    • ขอบคุณมากสำหรับ สรุป TL;DR สั้นๆ แบบนี้ช่วยลดกำแพงในการเข้าถึงบทความและทำให้อยากอ่านต่อ
    • ทิ้งความเห็นไว้ว่า “ไม่น่าแปลกใจเลย (Quelle Surprise)”
  • clock_gettime() หลีกเลี่ยง context switch ผ่าน vDSO ได้ เลยเห็นร่องรอยนี้ใน flamegraph ด้วย
    • แต่ใช้ได้แค่กับ clock บางตัวเท่านั้น กรณีอย่าง CLOCK_VIRT หรือ CLOCK_SCHED ยังต้อง เรียก syscall อยู่
    • ถ้าดูใต้เฟรมของ vDSO ก็ยังมี syscall อยู่ ดูเหมือนว่ายังไม่มีการทำ fast path สำหรับ clock id บางตัว
    • CLOCK_THREAD_CPUTIME_ID สุดท้ายก็ต้องลงไปที่เคอร์เนล เพราะต้องอ้างอิง task struct
      ดูซอร์สเคอร์เนลที่เกี่ยวข้องได้ที่ posix-cpu-timers.c,
      cputime.c,
      gettimeofday.c
  • ถ้าใช้ PERF_COUNT_SW_TASK_CLOCK ก็สามารถวัดได้ที่ระดับประมาณ 8ns
    วิธีคืออ่านจาก shared page ผ่าน perf_event_mmap_page แล้วคำนวณเดลต้าด้วยการเรียก rdtsc
    เรื่องนี้มีเอกสารไม่มากและแทบไม่มี implementation แบบโอเพนซอร์ส
    • เป็นทริกที่เจ๋งมาก แต่อาจเหมาะกับ เธรดอายุยาว มากกว่า เพราะ perf_event ต้องมีการตั้งค่าและสิทธิ์ค่อนข้างเยอะ
    • มีคนถามว่าทำไมถึงต้องใช้ seqlock เพื่อป้องกันไม่ให้เกิด context switch ระหว่างค่าของหน้าเพจกับ rdtsc หรือไม่
      น่าจะเป็นโครงสร้างที่เช็กค่าหน้าเพจอีกครั้งหลัง rdtsc แล้วถ้ามีการเปลี่ยนก็ลองใหม่
      อนึ่ง clock_gettime เองก็เป็น virtual syscall ที่อาศัย vdso
    • clock_gettime ไม่ใช่ syscall แต่ใช้ vdso
  • Flamegraph เป็นเครื่องมือที่ยอดเยี่ยมจริงๆ
    เวลาดูจากโค้ดอย่างเดียวอาจเหมือนปกติดี แต่พอดู flamegraph แล้วมักจะเจอความรู้สึกแบบ “นี่มันอะไรเนี่ย?!”
    เคยเจอปัญหาหลายอย่าง เช่น การทำ initialization ที่ไม่ใช่ static initialization หรือการเรียก logger แค่บรรทัดเดียวแต่กลับก่อให้เกิด serialization ที่มีต้นทุนสูง
    • ผมชอบ icicle graph ด้วย มันสะสมจากทิศทางตรงข้ามกับ flamegraph ทำให้ดูคอขวดได้ง่ายเวลาเส้นทางหลายเส้นเรียกใช้ไลบรารีร่วมกัน
    • ถ้าเปิด ตัวอย่าง SVG นี้ ในแท็บใหม่ จะสามารถ ซูมแบบอินเทอร์แอ็กทีฟ ได้
    • การทดลองทำ performance profiling และ optimization เป็นหนึ่งในส่วนที่สนุกที่สุดของงานพัฒนา เพราะมักได้เจอความประหลาดใจว่า “ทำไมอันนั้นถึงช้าขนาดนี้?”
    • มีความเห็นว่าการจับคู่กันของ string parsing กับ memoization ฟังดูแปลก แต่ในความเป็นจริงปัญหาเกิดจากไม่ได้ cache การ parse regex pattern ที่มีต้นทุนสูง
    • สำหรับคนที่อยากเริ่มใช้ flamegraph เป็นครั้งแรก มีคนถามถึงแนวคิดพื้นฐานและจุดเริ่มต้น
  • น่าทึ่งที่ “เปิดภาพในแท็บใหม่” ให้ การโต้ตอบกับ SVG ได้จริง
    • ฟีเจอร์นี้เป็นเพราะ สคริปต์ FlameGraph ของ Brendan Gregg
      ปกติผมจะใช้ตัวสร้าง HTML ของ async-profiler แต่ครั้งนี้ใช้เครื่องมือของ Brendan เพื่อให้ได้ SVG ไฟล์เดียว
  • ผมคือผู้เขียนแพตช์ OpenJDK ที่พูดถึง โอเวอร์เฮดด้านหน่วยความจำ ของการอ่าน /proc, การทำ eBPF profiling และประวัติของ user-space ABI ที่เอกสารมีน้อย
    รายละเอียดเพิ่มเติมสรุปไว้ใน บล็อกโพสต์ของผม
    • มีคนถามว่าทำไม implementation เดิมถึงถูกทำไว้แบบนั้น การทำ file IO และ string parsing ทุกครั้งที่เรียกใช้นั้นไม่มีประสิทธิภาพ แต่ก็น่าจะมีเหตุผลในยุคนั้น
    • Jaromir อ่านบทความของผมแล้วบอกว่า “ผมก็เขียนร่างไว้ในช่วงเวลาเดียวกัน” แล้วก็ ลิงก์บทความของกันและกัน ผมดีใจที่เขาประเมินว่าบทความของผมเข้มงวดกว่า
  • การใช้ภาษาระบบอย่าง C หรือ C++ ไม่ได้แปลว่าจะเร็วเสมอไป ความเร็วต่างกันมากขึ้นอยู่กับว่า คุณกำลังทำอะไรอยู่
  • การอ่านผ่าน vDSO เร็วกว่าอย่างมาก เพราะหลีกเลี่ยงการสลับเข้าเคอร์เนล, การ serialize บัฟเฟอร์ และกระบวนการ parsing
  • มีการแชร์คำพูดว่า “ถ้ามันเร็วขึ้น 2 เท่า อาจเป็นเพราะคุณทำอะไรฉลาดๆ แต่ถ้าเร็วขึ้น 100 เท่า ก็แค่ เลิกทำเรื่องโง่ๆ เท่านั้นเอง”
    ทวีตต้นทาง
  • ทีม QuestDB อยู่ในระดับ แนวหน้าของวงการ นี้จริงๆ ทั้งคนทั้งซอฟต์แวร์ยอดเยี่ยมมาก
    บล็อกของ Jaromir ก็ยอดเยี่ยมจริงๆ