3 คะแนน โดย GN⁺ 2025-03-11 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • โปรเจกต์ CPython เพิ่งนำกลยุทธ์การอิมพลีเมนต์ใหม่สำหรับไบต์โค้ดอินเทอร์พรีเตอร์มาใช้ โดยผลลัพธ์เบื้องต้นแสดงให้เห็นว่าประสิทธิภาพดีขึ้นเฉลี่ย 10-15% บนหลากหลายแพลตฟอร์ม
  • อย่างไรก็ตาม การเพิ่มขึ้นของประสิทธิภาพนี้ส่วนใหญ่เป็นผลจากการหลบเลี่ยงปัญหา regression ใน LLVM 19 เมื่อเทียบกับเกณฑ์อ้างอิงที่ดีกว่า (เช่น GCC, clang-18, หรือ LLVM 19 ที่ใช้แฟล็กปรับแต่งเฉพาะ) การเพิ่มขึ้นของประสิทธิภาพลดลงเหลือ 1-5%

ผลลัพธ์ด้านประสิทธิภาพ

  • ได้ทำเบนช์มาร์กหลายบิลด์ของ CPython อินเทอร์พรีเตอร์โดยใช้คอมไพเลอร์และตัวเลือกการตั้งค่าหลายแบบ ทดสอบบนเซิร์ฟเวอร์ Intel และ Apple M1 Macbook Air
  • ทุกบิลด์ใช้ LTO และ PGO และใช้ค่าเฉลี่ยที่รายงานโดย pypeformance/pyperf compare_to โดยอ้างอิง clang18 เป็นฐาน
  • การเปรียบเทียบประสิทธิภาพของคอมไพเลอร์
    • Apple M1 Macbook Air :
      • clang18: ค่าฐาน
      • clang19: ช้ากว่า 1.12 เท่า
      • clang19.taildup: ช้ากว่า 1.02 เท่า
      • clang19.tc: ช้ากว่า 1.00 เท่า
      • gcc: N/A
  • อินเทอร์พรีเตอร์แบบ tail-call ยังแสดงการปรับปรุงความเร็วเมื่อเทียบกับ clang-18 แต่การลดลงของความเร็วจากการย้ายไปใช้ clang-19 นั้นรุนแรงกว่าอย่างชัดเจน

LLVM regression

พื้นหลังแบบย่อ

  • ไบต์โค้ดอินเทอร์พรีเตอร์แบบดั้งเดิมประกอบด้วยคำสั่ง switch ภายในลูป while โดยคอมไพเลอร์ส่วนใหญ่จะคอมไพล์ switch ให้เป็นตารางกระโดด
  • คอมไพเลอร์ C สมัยใหม่รองรับแพตเทิร์นที่นำ address ของ label มาใช้เป็น "computed goto" โดย CPython ใช้แพตเทิร์นนี้ก่อนงาน tail-call

LLVM 19 regression

  • LLVM 19 เพิ่มข้อจำกัดใน tail-duplication pass โดยจะหยุดการทำซ้ำเมื่อขนาด IR เกินขีดจำกัดหนึ่ง ส่งผลให้ใน CPython การกระโดด dispatch ทั้งหมดถูกรวมเข้าด้วยกัน และทำให้เป้าหมายของอิมพลีเมนเทชันแบบ goto ที่อิง computed goto สูญเปล่าโดยสิ้นเชิง

ความผิดปกติเพิ่มเติม

  • แม้จะมั่นใจว่าการเปลี่ยนแปลงของตรรกะการทำซ้ำสำหรับ tail call เป็นสาเหตุของ regression แต่ก็ยังอธิบาย ขนาด ของ regression ได้ไม่ครบถ้วน
  • บนโปรเซสเซอร์สมัยใหม่ การเพิ่มความเร็วราว 2-4% ถือว่าเกิดขึ้นได้ทั่วไปมากกว่า

จำเป็นต้องมี computed goto หรือไม่?

  • เบนช์มาร์ก clang19.nocg อ้างว่าทำงาน เร็วกว่า clang19 ซึ่งแสดงให้เห็นว่าคอมไพเลอร์สามารถทำ optimization แบบเดียวกันได้แม้ใช้อินเทอร์พรีเตอร์ที่อิง switch

การแก้ไข

  • LLVM pull request 114990 ได้แก้ regression นี้แล้ว และการแก้ไขดังกล่าวช่วยกู้คืนประสิทธิภาพกลับมาได้ตามที่คาดไว้

ข้อคิดทบทวน

ว่าด้วยการทำเบนช์มาร์ก

  • เมื่อต้องปรับแต่งระบบ จะมีการจัดทำทั้งตัวเบนช์มาร์กและวิธีวิทยาการทำเบนช์มาร์ก เพื่อประเมินการเปลี่ยนแปลงที่เสนอ
  • เบนช์มาร์กต้องอาศัยสมมติฐานและความเชื่อมากขึ้น เมื่อต้องการนำจุดข้อมูลเฉพาะไปสรุปเป็นภาพรวม

ค่าฐาน

  • เมื่อเสนอวิธีแก้ปัญหาหรือแนวทางใหม่ การเปรียบเทียบกับ "แนวทางที่รู้กันว่าดีที่สุดในปัจจุบัน" ถือเป็นเรื่องปกติ

ว่าด้วยวิศวกรรมซอฟต์แวร์

  • ระบบซอฟต์แวร์มีความซับซ้อน เชื่อมโยงถึงกัน และเปลี่ยนแปลงอย่างรวดเร็ว
  • คอมไพเลอร์สำหรับ optimization ต้องอยู่ในภาวะตึงเครียดระหว่างการเคารพเจตนาของโปรแกรมเมอร์กับการทำโค้ดให้เหมาะสมที่สุด

คอมไพเลอร์สำหรับ optimization

  • แอตทริบิวต์ musttail แสดงถึงความสามารถคอมไพเลอร์รูปแบบใหม่ที่เกี่ยวข้องกับ optimization ซึ่งอาจมอบสไตล์ที่ทรงพลังกว่าในการเขียนโค้ดที่ไวต่อประสิทธิภาพ

อีกเรื่องหนึ่งเกี่ยวกับ nix

  • nix มีประโยชน์มากในโปรเจกต์นี้ และช่วยอย่างมากในการจัดการและบิลด์ Python อินเทอร์พรีเตอร์หลายเวอร์ชัน

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

 
GN⁺ 2025-03-11
ความคิดเห็นจาก Hacker News
  • สวัสดีครับ ผมเป็นผู้เขียน PR ที่นำ tail-calling interpreter เข้ามาใน CPython

    • ก่อนอื่นอยากกล่าวขอบคุณ Nelson ที่ใช้เวลาเกือบหนึ่งเดือนในการค้นหาต้นตอของปัญหานี้
    • อีกทั้งผมยังรู้สึกอับอายและเสียใจมากที่ทำผิดพลาดครั้งใหญ่เช่นนี้
    • แม้แต่ทีม CPython เองก็ไม่คาดคิดว่าคอมไพเลอร์ที่เราใช้จะมีบั๊กแบบนี้
    • ผมโพสต์บล็อกขอโทษไว้ที่นี่: ลิงก์
  • การทำ benchmark เป็นงานที่ยากมากจริง ๆ

    • ไม่นานมานี้ผมพบวิธีทำให้อัลกอริทึมเร็วขึ้นราว 15%
    • แต่ระหว่างการทดสอบ โค้ดเดิมกลับเร็วขึ้น 15% โดยที่ไม่ได้เรียกใช้ฟังก์ชันเวอร์ชันที่เร็วกว่าเลย
    • นี่เป็นปัญหาเรื่องการจัดวางโค้ดและหน่วยความจำ ซึ่งไปจัดแนวกับ CPU cache ได้ดีขึ้น
    • Casey Muratori กำลังทำซีรีส์ที่น่าสนใจเกี่ยวกับหัวข้อนี้อยู่
  • ขอชื่นชมผู้เขียนที่สืบหาความจริงของปัญหานี้จนพบ

    • tail-call interpreter ของ Python 3.14 ยังถือเป็นการปรับปรุงที่ดีอยู่
    • เหตุการณ์นี้สอนให้เห็นถึงความสำคัญของความเข้มงวดในการทำ benchmark และการทดสอบในสภาพแวดล้อมที่หลากหลาย
    • อีกทั้งตอนนี้ยังได้พบบั๊กของคอมไพเลอร์ที่อาจเป็นประโยชน์กับทุกคน
    • น่าสงสัยว่าผลลัพธ์ประเภท "เร็วขึ้น X%" จำนวนมากแค่ไหนที่จริง ๆ แล้วเกิดจาก benchmark artifact หรือ regression ที่ยังไม่ถูกค้นพบ
  • นี่เป็นตัวอย่างที่ดีว่า C ไม่ใช่ภาษาที่ "ใกล้เครื่อง" เสมอไป

    • clang-19 คอมไพล์ computed goto interpreter ได้ "ถูกต้อง" แต่สร้างผลลัพธ์ที่แตกต่างจากเจตนาของการ optimize โดยสิ้นเชิง
    • คอมไพเลอร์เวอร์ชันอื่นก็ใช้ optimization กับ interpreter แบบ switch() ที่ "ตรงไปตรงมา" เช่นกัน
  • จากการปรับวิธีที่คอมไพเลอร์จัดโครงสร้างลูป ทำให้ tail-call interpreter ไม่ได้มีประสิทธิภาพอย่างที่ประกาศไว้

    • สถาปัตยกรรมและเวอร์ชันของ CPU มีความสำคัญมาก
    • C abstract machine ไม่ได้อยู่ในระดับ low-level มากพอที่จะสื่อเจตนาได้อย่างถูกต้อง
    • การทำ interpreter implementation แบบระแวดระวังเป็นพิเศษบางแบบจึงย้อนกลับไปเขียนแอสเซมบลีโดยตรง
    • luajit ได้ทำระบบแมโครขึ้นมาเพื่อให้ implementation ของลูปแอสเซมบลีที่มีประสิทธิภาพสามารถพกพาข้ามสถาปัตยกรรมได้
  • การประเมินประสิทธิภาพของ Python build เป็นเรื่องยากมาก

    • เมื่อไม่นานมานี้ทีม astral แสดงให้เห็นว่า build ของ conda-forge เร็วกว่าบิลด์ส่วนใหญ่อื่น ๆ
    • น่าสนใจว่า tail-call interpreter จะทำงานร่วมกับ optimization อื่น ๆ ของ build อย่างไร
  • การอภิปรายที่เกี่ยวข้อง:

  • เป็นบทความที่ยอดเยี่ยม

    • ในบทความอ้างอิงชิ้นหนึ่งมีการระบุว่า 3.14.0a5 เร็วกว่า 3.13 อยู่ 1.12 เท่า
    • ผมสับสนว่า benchmark ถูกรันในสภาพที่มีโปรเซสอื่นโหลดเครื่องอยู่หรือไม่
    • benchmark ควรถูกดำเนินการในสภาพแวดล้อมที่ควบคุมอย่างเข้มงวดเพื่อกำจัดตัวแปรภายนอก
  • ไม่นานมานี้ผมทำ benchmark ตั้งแต่ Python 3.9 ถึง 3.13

    • ประสิทธิภาพดีขึ้นจนถึง 3.11 แต่ 3.12 และ 3.13 ช้ากว่า 3.11 ราว 10%
    • ตอนแรกคิดว่า benchmark ของตัวเองอาจยังไม่เพียงพอ แต่เมื่อ deploy ไปยังบริการหลักก็ยังสังเกตเห็นการเปลี่ยนแปลงแบบเดียวกัน
  • สงสัยว่าการ optimize แบบนี้เกี่ยวข้องกับ tail-call optimization อย่างไร

    • implementation ของ jump table ใน interpreter ไม่ควรส่งผลต่อการสร้าง stack frame