1 คะแนน โดย GN⁺ 5 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • มีการ merge การปรับปรุงการจัดการจำนวนเต็มแบบ non-ABI ในแบ็กเอนด์ LLVM และความหมายใหม่ของ @bitCast เข้าใน master branch ของ Zig เพื่อจัดการทั้งปัญหาการ optimize และความไม่สอดคล้องของพฤติกรรมภาษา
  • จำนวนเต็มความกว้างบิตตามใจชอบ เช่น u4, i13, u40 จะถูกจัดการเป็น bit-int ในค่า SSA แต่เมื่อเก็บลงหน่วยความจำจะเปลี่ยนเป็นการขยายไปเป็นจำนวนเต็มขนาด ABI
  • @bitCast เดิมมีลักษณะใกล้เคียงกับการตีความ byte ในหน่วยความจำใหม่ แต่คำนิยามใหม่จะตีความตาม อาร์เรย์บิตเชิงตรรกะ ของ type เพื่อลดการพึ่งพา endian
  • การเปลี่ยนแปลงนี้ขยายไปถึงแบ็กเอนด์ LLVM, C และการรัน comptime และได้ตรวจสอบจุดใช้งานที่เกี่ยวข้องใน standard library, compiler และ compiler_rt ด้วย
  • เมื่อ LLVM optimization ที่เคยพลาดไปกลับมาทำงานอีกครั้ง พบว่า compiler ของ Zig เองมี ประสิทธิภาพดีขึ้นประมาณ 5% และคาดว่า 0.17.0 อาจได้ runtime performance ดีขึ้นในบางส่วน

การเปลี่ยนแปลงการจัดการจำนวนเต็มความกว้างบิตตามใจชอบในแบ็กเอนด์ LLVM

  • เดิม Zig เคย lowering ชนิดจำนวนเต็มความกว้างบิตตามใจชอบ เช่น u4, i13, u40 ไปเป็นชนิด bit-int ของ LLVM IR อย่าง i4, i13, i40 โดยตรง
  • วิธีนี้ทำให้ความหมายของการแทนค่าในหน่วยความจำของ LLVM สร้างข้อจำกัดที่ไม่จำเป็นต่อ optimizer และเนื่องจาก Clang ไม่สร้าง LLVM IR แบบนี้ เส้นทางภายใน LLVM จึงไม่ได้รับการทดสอบอย่างเพียงพอ
  • ในช่วงไม่กี่ปีที่ผ่านมา พบกรณี optimization ที่ถูกพลาดไป และ miscompilation จริง
  • วิธีใหม่ยังคงใช้ชนิด bit-int สำหรับการจัดการค่า SSA แต่เมื่อเก็บลงหน่วยความจำจะ zero-extend หรือ sign-extend ไปเป็น ชนิดขนาด ABI เช่น i8, i16, i32
  • การ lowering นี้สอดคล้องกับวิธีที่ Clang lowering _BitInt(N) ของ C จึงคาดว่าจะเป็นเส้นทางที่ LLVM รองรับได้ดีกว่า

ข้อจำกัดของ @bitCast เดิม

  • @bitCast เดิมในเชิงแนวคิดใกล้เคียงกับพฤติกรรมต่อไปนี้
    • ดึง pointer ของค่าที่เป็น operand
    • cast pointer นั้นเป็น pointer ของชนิดปลายทาง
    • load ค่าจาก pointer นั้น
  • กล่าวคือคำนิยามเดิมใกล้เคียงกับการ ตีความ byte ใหม่ ในหน่วยความจำ มากกว่าโครงสร้างเชิงตรรกะของ type
  • เมื่อเวลาผ่านไป พฤติกรรมจริงก็เริ่มเบี่ยงออกจากคำนิยามนี้ และใน target ส่วนใหญ่ แม้ @sizeOf(u24) จะใหญ่กว่า @sizeOf([3]u8) แต่การ @bitCast [3]u8 เป็น u24 ก็ยังได้รับอนุญาต
  • แบ็กเอนด์ LLVM กำลัง implement ความหมายของ @bitCast ที่ยังไม่ได้ระบุสเปกไว้อย่างเพียงพอ และเมื่อเปลี่ยนวิธีเก็บชนิดจำนวนเต็มลงหน่วยความจำ ก็เกิด Illegal Behavior และ crash ใน test suite ของ compiler
  • แทนที่จะเพิ่ม logic ในแบ็กเอนด์ LLVM เพื่อเลียนแบบพฤติกรรมเดิม จึงเลือกแนวทาง implement คำนิยามใหม่ของ @bitCast ในภาพรวม

ความหมายใหม่ของ @bitCast

  • ความหมายใหม่นี้อิงจากข้อเสนอภาษา #19755 ที่ส่งและได้รับการยอมรับในปี 2024
  • ความหมายนี้ถูก implement ไว้แล้วในแบ็กเอนด์ self-hosted x86_64 และการเปลี่ยนแปลงครั้งนี้ขยายไปถึงแบ็กเอนด์ LLVM, C และการรัน comptime
  • @bitCast ใหม่ทำงานโดยอิงจาก ลำดับบิตที่แสดง type ในเชิงตรรกะ ไม่ใช่ byte ในหน่วยความจำ
    • u5 ประกอบด้วยบิตเชิงตรรกะ 5 บิต ตั้งแต่ least-significant bit ไปจนถึง most-significant bit
    • [2]u5 ประกอบด้วยบิตเชิงตรรกะ 10 บิต โดยมี 5 บิตของ element แรกตามด้วย 5 บิตของ element ที่สอง
  • การแปลงระหว่างจำนวนเต็มแบบง่าย เช่น เปลี่ยน u8 เป็น i8 ที่มีขนาดเท่ากัน บิตจะคงเดิม และบิตสูงสุดจะถูกตีความเป็นบิตเครื่องหมาย
  • ความหมายของ @bitCast ระหว่างชนิดจำนวนเต็มกับ packed struct หรือ packed union ก็ยังคงเดิม

พฤติกรรมที่เปลี่ยนไปใน array และ vector

  • จุดที่ความหมายใหม่แตกต่างจากเดิมคือเมื่อเกี่ยวข้องกับ aggregate type เช่น array และ vector
  • ตัวอย่างเช่น หาก @bitCast [2]u8 เป็น u16 ความหมายเดิมจะให้ผลลัพธ์ต่างกันตาม endian ของ target
    • บน target แบบ big-endian element แรกของ array จะกลายเป็น 8 บิตบน
    • บน target แบบ little-endian element แรกของ array จะกลายเป็น 8 บิตล่าง
  • ความหมายใหม่พิจารณาเฉพาะการแทนค่าบิตเชิงตรรกะ จึงไม่ขึ้นกับ endian และบนทุก target element แรกของ array จะกลายเป็น 8 บิตล่าง
  • โดยทั่วไปแล้วจะใกล้เคียงกับพฤติกรรมเดิมบน target แบบ little-endian มากกว่า
  • การแปลงที่ไม่เป็นรูปแบบทั่วไปก็ทำได้ เช่น แปลง [2]u3 เป็น @Vector(3, u2)
    • นำบิตเชิงตรรกะของ array มาต่อกัน แล้วอ่านทีละ 2 บิตเพื่อสร้าง element ของ vector
    • ยังใช้ @bitCast จำนวนเต็มเป็น @Vector(n, u1) เพื่อแยกเป็น vector ของบิตแต่ละตัวได้ด้วย

ข้อเสนอที่ถูกรวมเข้ามาด้วยและการ migration

  • ระหว่างงานนี้ ยังมีการ implement ข้อเสนอเล็ก ๆ ที่ได้รับการยอมรับและเกี่ยวข้องกับ @bitCast ด้วย
    • ห้าม @bitCast กับ vector ของ pointer: #18936
    • อนุญาต @bitCast สำหรับ enum: ส่วนหนึ่งของ #35602
  • เนื่องจากความหมายใหม่ต่างจากความหมายเดิมอย่างมีนัยสำคัญ จึงมีการตรวจสอบการใช้งาน @bitCast ใน support library เช่น standard library, compiler และ compiler_rt
  • PR ที่เกี่ยวข้องคือ codeberg.org/ziglang/zig/pulls/35711 และเมื่อ merge เข้า master ก็ได้ปิด issue หลายรายการไปพร้อมกัน
  • ความหมายที่เปลี่ยนไปและขั้นตอน migration ที่แนะนำจะถูกสรุปไว้ใน release notes ของ Zig 0.17.0

ผลด้านประสิทธิภาพที่คาดหวังใน 0.17.0

  • เป้าหมายเดิมคือการเปลี่ยน lowering ของจำนวนเต็ม non-ABI ในแบ็กเอนด์ LLVM ซึ่งประสบความสำเร็จในการทำให้ optimization ที่เคยพลาดกลับมาทำงาน
  • ผลลัพธ์ที่เกี่ยวข้องดูได้ที่ demonstrably successful
  • แม้ compiler ของ Zig เองจะไม่ได้ใช้จำนวนเต็มความกว้างบิตตามใจชอบภายในมากนัก แต่ด้วย optimization ที่ดีขึ้น ก็แสดงให้เห็น ประสิทธิภาพดีขึ้นประมาณ 5%
  • ใน 0.17.0 โค้ดบางส่วนอาจมี runtime performance ดีขึ้นเล็กน้อย

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

 
GN⁺ 5 시간 전
ความคิดเห็นจาก Lobste.rs
  • ในบทความบอกว่า การแทนบิตเชิงตรรกะ ไม่ขึ้นกับเอนดิแอน แต่คำอธิบายจริงดูเป็นวิธีแบบ little-endian อย่างชัดเจน ซึ่งไม่รองรับลำดับบิตหรือลำดับไบต์แบบ big-endian

    • ตรงนี้คำว่า ไม่ขึ้นกับเอนดิแอน น่าจะหมายถึงพฤติกรรมจะไม่เปลี่ยนไประหว่างสถาปัตยกรรม little-endian/big-endian
  • เป็นบันทึกความคืบหน้าการพัฒนาใหม่ลงวันที่ 25 มิถุนายน 2026 โดยบอกว่า semantics ของ @bitCast แบบใหม่ และการปรับปรุง LLVM backend ได้ถูกรวมเข้าในพูลรีเควสต์ล่าสุดแล้ว

  • น่าสนใจ แต่ก็สงสัยว่าโค้ดที่เขียนแบบด้านล่างบน เป้าหมาย big-endian ซึ่งแทบไม่ได้ทดสอบ อาจพังขึ้นมาแบบกะทันหันหรือเปล่า
    ถ้าเขียนเป็น pseudocode ที่ไม่ใช่ Zig:

    if target_is_little_endian {  
        my_int = @bitCast(my_array);  
    } else {  
        my_int = @bitCast([my_array[1], my_array[0]]);  
    }  
    
    • ฉันก็คิดแบบนั้นเหมือนกัน แต่สุดท้ายการเลื่อนการเปลี่ยนแปลงที่หลีกเลี่ยงไม่ได้ออกไปก็มีแต่จะทำให้ปัญหาใหญ่ขึ้น
      เอาเข้าจริงคงไม่ใช่ปัญหาใหญ่มาก เพราะจาก @bitCast หลายพันจุด ในรีโพของ Zig ดูเหมือนว่าจะมีไม่ถึง 100 จุดที่ได้รับผลจากการเปลี่ยนแปลงนี้
      พูดตามตรงก็ไม่คิดว่าผู้ใช้ Zig ส่วนใหญ่รู้ชัดนักว่า @bitCast ทำงานอย่างไรในการแปลงระหว่างอาร์เรย์/เวกเตอร์กับสเกลาร์ ก่อนหน้านี้โค้ดหลายส่วนอาจถูกทดสอบแค่บนระบบของผู้เขียนและทำงานได้เฉพาะบน little-endian แต่ตอนนี้จะกลายเป็นว่าทำงานได้ทุกที่แทน
  • ในฐานะอดีตโปรแกรมเมอร์ C ฉันจำได้ว่า bit field ของ C ไม่ค่อยได้รับความนิยม เพราะพฤติกรรมของมันไม่พกพาข้ามสถาปัตยกรรม
    semantics @bitCast แบบใหม่ของ Zig เป็น semantics เชิงนามธรรมที่พกพาได้ ซึ่งให้ผลลัพธ์เหมือนกันบนสถาปัตยกรรมที่ต่างกัน จึงรู้สึกว่าเป็นทิศทางที่จำเป็นมาก
    ช่วงนี้ฉันกำลังออกแบบ bit field และ bit cast ในภาษาของตัวเองอยู่พอดี เลยตั้งใจจะอ่านเอกสารการออกแบบและการ implement ของ Zig ให้ละเอียดขึ้น เพื่อให้ชัดว่าโค้ดของฉันควรทำงานอย่างไร

    • ทางเลือกหลักของ Zig สำหรับ C bit field น่าจะเป็น packed struct และ packed union ซึ่งทั้งคู่ถูกนิยามให้สอดคล้องกับนิยาม @bitCast แบบใหม่ได้ดี
      packed struct เป็นวิธีนำบิตของฟิลด์ไปอัดลงใน “จำนวนเต็มฐาน” เช่น ถ้าฟิลด์คือ bool, u6, i9 และจำนวนเต็มฐานคือ u16 ก็จะได้ว่า บิตต่ำสุดของ u16 คือ bool, 6 บิตถัดไปคือ u6, และอีก 9 บิตที่เหลือคือ i9 ดังนั้น packed struct ของ Zig จึงค่อนข้างคล้าย syntactic sugar ที่ครอบอยู่บนการ shift และ mask หลายชุด
      packed union ก็มีจำนวนเต็มฐานเช่นกัน แต่ทุกฟิลด์ต้องใช้จำนวนบิตเท่ากับจำนวนเต็มฐานแบบพอดีเป๊ะ ดังนั้นการเก็บค่าผ่านฟิลด์หนึ่งแล้วไปอ่านผ่านอีกฟิลด์จึงแทบจะเหมือนกับ @bitCast ภายใต้ semantics ใหม่ทุกประการ เพียงแต่ฟิลด์ของ packed union/packed struct จะมีชนิดเป็นอาร์เรย์หรือเวกเตอร์ไม่ได้
      โดยส่วนตัวฉันคิดว่าเครื่องมือพวกนี้เหมาะมากสำหรับการแสดง “โครงสร้างที่เกี่ยวกับบิต” คุณสามารถแพ็กหลายค่าเข้า packed struct เพื่อใช้แบบ C bit field ได้ และเพราะมันเป็น syntactic sugar บนการดำเนินการระดับบิต จึงใช้แทน bit flag ที่ใน C มักต้องจัดการด้วยแมโครที่ไม่ type-safe ได้อย่างเรียบร้อย
      ตัวอย่างเช่น แฟลกสิทธิ์เข้าถึงแบบ RWX ใน C อาจรับผ่านแมโคร ACCESS_READ, ACCESS_WRITE, ACCESS_EXEC และ API ที่รับ uint8_t แต่ใน Zig คุณสามารถประกาศ Access = packed struct(u8) พร้อมฟิลด์ read, write, exec, reserved แล้วให้ API รับ Access ได้เลย
      เมื่อใช้ packed struct และ packed union ก็ยังสามารถอธิบายการจัดวางบิตที่ค่อนข้างประหลาดได้ด้วย ในฟอร์แมตอ็อบเจ็กต์ Mach-O มีเอนทรีตารางสัญลักษณ์ที่มีฟิลด์ n_type แปลก ๆ ซึ่งน่าจะมาจากเหตุผลทางประวัติศาสตร์ โดยสามารถโมเดลมันเป็น packed union(u8) ที่ภายในมี bits: packed struct(u8) และ stab: enum(u8) ได้
      เวลาจัดการค่า n_type นี้ก็ไม่ต้อง shift หรือ mask ด้วยมือ เพียงเช็ก n_type.bits.is_stab != 0 แล้วถ้าเป็นจริงก็ switch ที่ n_type.stab ไม่เช่นนั้นก็ดูฟิลด์อื่นใน n_type.bits ได้เลย และในทางกลับกันก็สร้างค่าได้แบบ .{ .stab = .gsym } หรือ .{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } }
      แม้จะยาวออกนอกประเด็นจากหัวข้อบทความเดิมไปหน่อยเพราะเป็นฟีเจอร์ภาษาอีกตัวหนึ่ง แต่ถ้ากำลังมองหาอะไรไว้ใช้อ้างอิงในการออกแบบภาษาใหม่ ลองใช้ packed struct และ packed union ของ Zig โดยตรงก็น่าจะดี มันเป็นเครื่องมือที่เรียบง่ายแต่ค่อนข้างดีทีเดียว