เอกสารสองหน้าของ Epic ให้คำตอบคนละอย่างเรื่อง TObjectPtr และทั้งคู่ถูก
คุณเปิดโปรเจกต์ UE5 แล้วเจอ UPROPERTY ที่เป็น raw pointer อยู่สองร้อยจุด
คำถามคือควรแปลงเป็น TObjectPtr ไหม และเอกสารทางการของ Epic ตอบคำถามนี้ไว้สองแบบ
บนหน้าคนละหน้า
สองประโยคที่วางข้างกันแล้วขัดกัน
หัวข้อที่มีชื่อว่า “สองประโยคที่วางข้างกันแล้วขัดกัน”Migration Guide อธิบายว่าการเลือกเป็นเรื่องที่ทำหรือไม่ทำก็ได้
เพราะ TObjectPtr จะกลายเป็น raw pointer ในบิลด์ที่ไม่ใช่ editor ซึ่งจะ
not impact the behavior or performance of a shipped product
หน้า incremental garbage collection เขียนถึงเรื่องเดียวกันว่ามันเป็นเรื่อง
critical to convert all UPROPERTY instances to use TObjectPtr
และบอกผลของการไม่ทำไว้ตรง ๆ ว่า GC อาจ
reclaim some of UObjects’ memory too early
อ่านเรียงกันสามประโยคแล้วมันไปคนละทาง ประโยคแรกบอกว่าไม่มีผล ประโยคที่สามบอกว่า หน่วยความจำหายไปตอนที่ยังใช้อยู่
ทั้งสามประโยคจริง และหน้านี้อธิบายว่าทำไม
สิ่งที่เปลี่ยนไม่ใช่คำตอบ แต่เป็นคำถาม
หัวข้อที่มีชื่อว่า “สิ่งที่เปลี่ยนไม่ใช่คำตอบ แต่เป็นคำถาม”ประโยคของ Migration Guide ตอบคำถามว่า “ตัวแทนของ pointer ในหน่วยความจำต่างกันไหม”
คำตอบคือไม่ต่าง ในบิลด์ที่ไม่ใช่ editor TObjectPtr ถูกลดรูปเป็น raw pointer จริง ๆ
ขนาดเท่ากัน การอ่านค่าเท่ากัน ไม่มีชั้นอ้อมเพิ่ม
ประโยคของหน้า GC ตอบคำถามอีกข้อหนึ่งที่ ยังไม่มีอยู่ตอนที่ประโยคแรกถูกเขียน: “ตอน GC กำลังเดินอยู่ อะไรบอกมันว่าคุณเพิ่งผูก reference ใหม่”
เอกสารเขียนไว้ว่า การกำหนดค่าให้ UPROPERTY ที่เป็น TObjectPtr นั้น
immediately marks the object as reachable
นั่นคือ write barrier — จุดที่ code ของเอนจินได้แทรกตัวเข้าไปในจังหวะที่คุณเขียนค่า
raw pointer ไม่มีจังหวะนั้น การกำหนดค่าให้ raw pointer คือการย้าย byte แปดตัว ไม่มีฟังก์ชันไหนถูกเรียก จึงไม่มีที่ให้เอนจินแทรกอะไรลงไปได้เลย
ทำไม GC แบบเดิมไม่ต้องการ barrier แต่แบบใหม่ต้องการ
หัวข้อที่มีชื่อว่า “ทำไม GC แบบเดิมไม่ต้องการ barrier แต่แบบใหม่ต้องการ”GC แบบเดิมของ Unreal เดินหา reference ให้จบภายในจังหวะเดียว โลกไม่ขยับระหว่างนั้น สิ่งที่มันเห็นตอนเริ่มจึงเป็นสิ่งเดียวกับตอนจบ
incremental reachability แบ่งงานนั้นออกเป็นหลายรอบคั่นด้วยเฟรมของเกม · code ของคุณรันอยู่ระหว่างรอบ และผูก reference ใหม่ได้
สมมติว่า GC เดินผ่านอ็อบเจกต์ A ไปแล้วในรอบแรก โดยตอนนั้น A ยังไม่ชี้ไปหา B
· เฟรมถัดมา code ของคุณสั่ง A->Target = B · GC ไม่ได้ย้อนกลับไปดู A อีก
มันจึงไม่เคยเห็นเส้นทางที่ไปถึง B และถ้าไม่มีใครอื่นถือ B ไว้ B จะถูกเก็บกวาด
ทั้งที่ A กำลังชี้อยู่
barrier คือสิ่งที่ทำให้บรรทัด A->Target = B บอก GC ได้ว่า B ยังเข้าถึงได้นะ
· และมันจะบอกได้ก็ต่อเมื่อ Target เป็น TObjectPtr
เงื่อนไขข้อเดียวที่ตัดสินว่าประโยคไหนใช้กับคุณ
หัวข้อที่มีชื่อว่า “เงื่อนไขข้อเดียวที่ตัดสินว่าประโยคไหนใช้กับคุณ”ประโยคของหน้า GC ผูกกับฟีเจอร์ที่ ต้องเปิดเอง ไม่ใช่ค่าตั้งต้น · เอกสารระบุว่า Epic
initially releasing this feature as experimental
การเปิดใช้ทำผ่านค่าใน DefaultEngine.ini คือ gc.AllowIncrementalReachability
กับ gc.AllowIncrementalGather
| โปรเจกต์ของคุณ | ประโยคที่ใช้ |
|---|---|
| ไม่ได้เปิด incremental reachability | Migration Guide ถูก การเลือกไม่กระทบ shipped product |
| เปิด incremental reachability | หน้า GC ถูก UPROPERTY ที่เป็น raw pointer กลายเป็นบั๊กหน่วยความจำ |
คนที่ตัดสินใจว่า “ไม่ต้องแปลงก็ได้” มักตัดสินตอนย้ายโปรเจกต์จาก UE4 ซึ่งเป็นตอนที่ Migration Guide ถูกอ่านมากที่สุด · ส่วนคนที่เปิด incremental reachability คือคนที่กำลัง ไล่แก้ hitch ของ GC ซึ่งเกิดทีหลังเป็นปี และมักไม่ใช่คนเดียวกัน
สวิตช์สองบรรทัดใน DefaultEngine.ini จึงเปลี่ยนสถานะของ UPROPERTY ทุกตัวในโปรเจกต์
ย้อนหลัง โดยไม่มีอะไรเตือน เพราะ code ที่เขียนไว้เดิมยังคอมไพล์ผ่านเหมือนเดิมทุกบรรทัด
คำที่ต้องไม่พูด
หัวข้อที่มีชื่อว่า “คำที่ต้องไม่พูด”มีคำอธิบายที่แพร่หลายว่า TObjectPtr “ไม่ได้ถือ ownership มันเป็นแค่ตัวช่วยตอน editor”
ครึ่งหลังพอใช้ได้ ครึ่งแรกผิด · หน้า Object Pointers เรียก
TObjectPtr ที่เป็น UPROPERTY ว่าเป็น
strong reference to an object
สิ่งที่ทำให้มันเป็น strong reference คือการที่มันเป็น UPROPERTY และถูก reflection
มองเห็น ไม่ใช่ตัว TObjectPtr เอง · raw pointer ที่เป็น UPROPERTY ก็ถือ reference
เหมือนกัน ความต่างอยู่ที่ barrier ไม่ได้อยู่ที่ ownership
ตรวจเองได้ ไม่ต้องเชื่อหน้านี้
หัวข้อที่มีชื่อว่า “ตรวจเองได้ ไม่ต้องเชื่อหน้านี้”ทุกข้ออ้างข้างบนอยู่บน 3 หน้าของ dev.epicgames.com และเปิดอ่านได้ฟรี ไม่ต้องมีสิทธิ์เข้าซอร์ส
- เปิดสองหน้าแรก แล้วค้นประโยคที่ยกมาในหน้านั้นตรง ๆ
- ในโปรเจกต์ของคุณ ค้น
DefaultEngine.iniด้วยคำว่าAllowIncrementalReachabilityถ้าไม่มี แปลว่าคุณอยู่แถวแรกของตาราง - เปิด file
.generated.hของ class ที่มีUPROPERTYแล้วดูว่า reflection เก็บอะไรไว้บ้าง
สิ่งที่หน้านี้ไม่ได้วัด
หัวข้อที่มีชื่อว่า “สิ่งที่หน้านี้ไม่ได้วัด”- ไม่มีบรรทัดไหนของหน้านี้รัน Unreal ทุกข้ออ้างเป็นการยกเอกสารของ Epic ต่างจากหน้าอื่นของเว็บนี้ที่ตัวเลขมาจากการรันจริง
- ไม่ได้วัดว่า GC เก็บของเร็วเกินไปจริงไหม เอกสารบอกว่าเป็นไปได้ หน้านี้ไม่ได้ทำให้มันเกิด
- ไม่ได้วัดต้นทุนของ barrier เอกสารไม่ได้ให้ตัวเลขนั้น และเว็บนี้ไม่เดาตัวเลข
สิ่งที่ทำแทนคือ ตัววัดที่ยิงทั้ง 3 หน้าแล้วยืนยันว่าประโยคที่ยกมายังอยู่จริง
· อยู่ที่ scripts/unreal-citations/ รันด้วย npm run verify:unreal-citations
· วันที่ Epic เขียนประโยคใหม่ ด่านนี้แดง และหน้านี้จะถูกทบทวนก่อนที่คุณจะเจอคำที่ไม่มีอยู่แล้ว