Zig ปรับปรุงความหมายใหม่ของ @bitCast และแบ็กเอนด์ LLVM
(ziglang.org)- มีการ 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ใน 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 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
ในบทความบอกว่า การแทนบิตเชิงตรรกะ ไม่ขึ้นกับเอนดิแอน แต่คำอธิบายจริงดูเป็นวิธีแบบ little-endian อย่างชัดเจน ซึ่งไม่รองรับลำดับบิตหรือลำดับไบต์แบบ big-endian
เป็นบันทึกความคืบหน้าการพัฒนาใหม่ลงวันที่ 25 มิถุนายน 2026 โดยบอกว่า semantics ของ
@bitCastแบบใหม่ และการปรับปรุง LLVM backend ได้ถูกรวมเข้าในพูลรีเควสต์ล่าสุดแล้วน่าสนใจ แต่ก็สงสัยว่าโค้ดที่เขียนแบบด้านล่างบน เป้าหมาย big-endian ซึ่งแทบไม่ได้ทดสอบ อาจพังขึ้นมาแบบกะทันหันหรือเปล่า
ถ้าเขียนเป็น pseudocode ที่ไม่ใช่ Zig:
เอาเข้าจริงคงไม่ใช่ปัญหาใหญ่มาก เพราะจาก
@bitCastหลายพันจุด ในรีโพของ Zig ดูเหมือนว่าจะมีไม่ถึง 100 จุดที่ได้รับผลจากการเปลี่ยนแปลงนี้พูดตามตรงก็ไม่คิดว่าผู้ใช้ Zig ส่วนใหญ่รู้ชัดนักว่า
@bitCastทำงานอย่างไรในการแปลงระหว่างอาร์เรย์/เวกเตอร์กับสเกลาร์ ก่อนหน้านี้โค้ดหลายส่วนอาจถูกทดสอบแค่บนระบบของผู้เขียนและทำงานได้เฉพาะบน little-endian แต่ตอนนี้จะกลายเป็นว่าทำงานได้ทุกที่แทนในฐานะอดีตโปรแกรมเมอร์ C ฉันจำได้ว่า bit field ของ C ไม่ค่อยได้รับความนิยม เพราะพฤติกรรมของมันไม่พกพาข้ามสถาปัตยกรรม
semantics
@bitCastแบบใหม่ของ Zig เป็น semantics เชิงนามธรรมที่พกพาได้ ซึ่งให้ผลลัพธ์เหมือนกันบนสถาปัตยกรรมที่ต่างกัน จึงรู้สึกว่าเป็นทิศทางที่จำเป็นมากช่วงนี้ฉันกำลังออกแบบ bit field และ bit cast ในภาษาของตัวเองอยู่พอดี เลยตั้งใจจะอ่านเอกสารการออกแบบและการ implement ของ Zig ให้ละเอียดขึ้น เพื่อให้ชัดว่าโค้ดของฉันควรทำงานอย่างไร
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 โดยตรงก็น่าจะดี มันเป็นเครื่องมือที่เรียบง่ายแต่ค่อนข้างดีทีเดียว