V8 Hidden Class

Have No Shell
3 min readNov 11, 2017

--

สวัสดีครับทุกโค๊นนนน ในที่สุดก็ได้ปล่อยภาคต่อของซีรีย์ V8 ซักที
ต้องขออภัยจริงๆ ช่วงนี้ไม่ค่อยมีเวลาเลยครับ
เพราะใช้เวลาไปกับสัตว์เลี้ยงแสนรัก 98.7% ที่ร้องโวยวายลั่นบ้านเวลาหิวนม ประเด็นคือ นางหิวนมทุกๆ 3 ชั่วโมง! ถ้าทักผมแล้วผมไม่ทักตอบแสดงว่าผมอยู่ในโหมดประหยัดพลังงานนะครับ ฮ่าๆๆ

แต่วันนี้ผมกลับมาแล้ว กลับมาอย่างยิ่งใหญ่ เพราะมีหลายคนรออย่างใจจดใจจ่อถึงการมาของบล๊อคที่มีสาระแบบนี้ (เอาจริงๆ ไม่มีเล๊ยยยย ฮ่วย! แต่ถ้ามี ขอเสียงเป็นกำลังใจให้พี่เบิร์ดหน่อยเร้วววว)

วันนี้น้ำน้อย ย่อมแพ้ไฟ .. ไม่ใช่!

วันนี้น้ำน้อย แบบเส้นเล็กต้มยำขลุกขลิก .. ไม่ใช่!

วันนี้น้ำน้อย ต้องเสียสละถุงเท้า ไม่ใช่เว้ยยยยยยยย!! ชักจะไปกันใหญ่ละ ๕๕+

เอาจริงๆ วันนี้เนื้อหาน่าสนใจ ผมจะพยายามไม่ออกทะเลบ่อยๆละกันนะครับ

เอาล่ะ ฟู่ววว.. หายใจเข้าลึกๆ และผ่อนหายใจออกช้าๆ

(ถ้าเราอ้าปากกว้างๆ แล้วพ่นลมออก ลมที่ออกจากปากจะเป็นลมร้อน
แต่ถ้าเราทำปากจู๋ แล้วพ่นลมออก ลมที่ออกมาจะเป็นลมเย็น — ไม่เกี่ยวอะไรกับเรื่องนี้เลย)

บล๊อคที่แล้ว ผมเกริ่นว่า V8 มีการ Optimize หลายวิธี วิธีแรกคือการ Inline ซึ่งก็คือการเอาเนื้อฟังก์ชั่นที่เราเรียก มาแปะไว้เลย ทำให้ไม่ต้องเสียเวลาไปสร้าง Stack ในขณะที่กระโดดไปทำงานในฟังก์ชัน

วันนี้คืออีกวิธีหนึ่ง ที่ Google engineer เค้าออกแบบขึ้นมา

จากปัญหาของ JavaScript ซึ่งเป็นภาษาที่จะเพิ่มตัวแปรเมื่อไหร่ ตอนไหน ที่ไหน อย่างไรก็ได้ เช่น

จากโค้ดข้างบน จะเห็นว่า ผมเติม everyThingIsLodash เข้าไปใน object tim
และเติม deskSmasher เข้าไปใน object victor
นี่ล่ะครับปัญหาเลย เพราะอะไร ล่ะ?

ก็เพราะว่า ด้วยความที่ JavaScript มันเป็นภาษา Dynamic นี่แหละครับ เพิ่มตัวแปรได้ แก้ตัวแปรได้ แตกต่างกับ Compile language ที่มีระบุ Type ชัดเจนตั้งแต่ตอน Compile เช่น
ใน C# เราเขียนแบบนี้

Class Hello {
int A;
long B;
int C;
}

พอเราสร้างตัวแปร Hello ขึ้นมา ก็จะมี Properties A, B, C เรียงต่อกันประมาณนี้

ตัวอย่าง Memory Offset สำหรับ Compile Language

ทีนี้ถ้าจะเข้าถึงตัวแปร C ก็สามารถกระโดดไปที่ตำแหน่งของ C ได้ทันที (เพราะเรารู้ว่า A และ B เป็น Type อะไร มีขนาดเท่าไหร่ ก็เลยสามารถหา Offset ของ c ได้ทันที)

แต่พอมาเป็น JavaScript มันกระโดดไปที่ C ไม่ได้เพราะมันไม่รู้ว่าอยู่ตรงไหน เป็นเพราะว่ามันไม่มี Type นั่นเอง(จะพูดให้ถูกก็คือ เราจะรู้ Type ก็ตอน Runtime) แล้วทีนี้จะกระโดดเข้าไปถึง C ได้ยังไงกันล่ะ และจะทำยังไงให้เร็ว ?

ทีม Engineer ของ V8 ก็เลยคิดวิธีนึงขึ้นมา แล้วตั้งชื่อให้มันเท่ห์ๆว่า Hidden Class เพื่อช่วยให้เข้าถึงตัวแปรได้เร็วขึ้น มีการทำงานดังนี้จ๊ะ

หลักการของ Hidden Class ก็คือเจ้า V8 จะแอบสร้าง Class ลับๆ ให้ Object ทุก Object ที่เราใช้ (อย่าลืมว่า V8 เขียนด้วย C++ นะครับ สามารถสร้างคลาสได้ อิอิ)
และ Class ที่สร้างขึ้นมาก็จะแอบเก็บข้อมูลว่า ตัวแปรแต่ละตัวอยู่ตำแหน่งไหน เพื่อเวลาต้องการใช้ตัวแปรไหน ก็สามารถเข้าไปเอาค่ามาได้ทันที ไม่เสียเวลาหาทุกตัว

ต่อไปนี้ผมจะขอเอาขั้นตอนของ V8 ตอนที่จะสร้าง Hidden Classes มายกตัวอย่างให้ดูกันครับ น่าจะทำให้เข้าใจมากขึ้น

สมมติว่าผมมีโค้ดแบบนี้

จากโค้ดด้านบน V8 จะสร้าง Hidden initial Class ให้สำหรับทุกฟังก์ชัน (ตั้งชื่อว่า Class 0 — คล้ายๆกับโปรเจกซีโร่ ที่ผลิตมนุษย์กลายพันธุ์)

ต่อมา ก็รันบรรทัดที่ 6 สร้าง Object Point ขึ้นมา แล้วโยน 11, 22 เข้าไปให้ Constructor ของ Point

ขั้นตอนนี้ขอขัดจังหวะ เพื่ออธิบายนิดนึงครับ
Object ใดๆใน JavaScript จะประกอบไปด้วย Pointer 2 ประเภท คือ Hidden class pointer กับ Backing storage pointer
- Hidden class pointer ก็เอาไว้ชี้ไปยัง Hidden class ของ object นั้น
- Backing storage pointer นี้เอาไว้ชี้ไปยังข้อมูลจริงๆ (ตรงนี้มี details อีกนิดหน่อยครับ เช่น เก็บ Int, เก็บ Double หรือเก็บ Object ก็จะแตกต่างกันอยู่นิดหน่อย แต่ผมไม่ขอพูดในที่นี้นะครับ ใครสนใจสามารถหาอ่านได้โดย google ว่า V8 backing storage หรือไม่ก็มาคุยกันได้ครับเดี๋ยวให้เบอร์ห้องเอาไว้ 123/23x)

ต่อไปก็รับค่า 11 ไปใส่ใน this.x
เจ้า V8 ก็จะสร้าง Hidden Class ใหม่ โดยปรับปรุงมาจาก Hidden Class 0
หลังจากนั้นก็อัพเดท Hidden Class Pointer ใน p1 ให้ไปชี้คลาสใหม่
และก็ใส่ค่า 11 เข้าไปใน Backing storage offset 0 ซะ !!

ต่อไปก็

พอเพิ่มตัวแปร y เข้ามา ก็อัพเดท Hidden Class ใหม่ ใส่ค่าไปใน Backing storage แบบนี้ไปเรื่อยๆครับ

สุดท้ายแล้ว เราจะได้รูปนี้ออกมา

สังเกตนะครับ ว่า p1 และ p2 แชร์ Hidden Class ตัวเดียวกันอยู่ อุ๊งงงๆๆ

หลังจากนั้น ถ้าเรามีการเรียกหา p1.y
เจ้า V8 ก็แค่หาใน Hidden Class ของ p1
แล้วไปดูว่า y อยู่ offset ที่เท่าไหร่ จากนั้นก็ไปดึงค่ามาจาก Backing storage

เพียงเท่านี้จะทำให้เข้าถึงตัวแปรได้เร็วกว่าแบบไล่หาทุกตัวแล้ว
แต่ถ้าถามเรื่อง performance ตอนแรกจะเสียเวลาสร้าง Hidden Class เยอะแยะมากมาย แต่ลองนึกถึง p2 สิครับ ไม่จำเป็นต้องสร้างอะไรเลย แค่ reuse Hidden class ที่มีอยู่แล้วเท่านั้นเอง

เพราะฉะนั้น เราควรจะ Reuse Hidden class ให้เยอะที่สุด เพื่อความรวดเร็ว
- เราควรจะสร้าง Property ใน Constructor
- เราควรจะเรียงลำดับการสร้าง Property ให้เหมือนเดิม (มันจะได้ใช้ Hidden class เดิมได้)

ฟู่วววว …

บล๊อคนี้มีสาระมากเกินมาตรฐานจริงๆ
คราวต่อไปต้องลดสาระลงหน่อยซะแล้ว :)

คราวหน้า เรามีนัดกันเรื่อง Optimization ใน V8 ต่อนะครับ
ทีนี้เราจะได้รู้ว่าเจ้า Hidden Class เนี่ย มีประโยชน์อะไรได้อีก ?
เฉลย .. มันเป็นตัวช่วยในการทำ Inline Caching (IC) (เอ๊า !? ทำไมทำงี้อะ)

ขอบคุณที่อ่านมาถึงบรรทัดนี้นะครับ
สำหรับวันนี้ ขอตัวลาไปก่อน … ลูกร้องหิวนมอีกแล้ว … ราตรีสวัสดิ์ครับ T-T

ปล. V8 Execution Pipeline part 3 จะมาในอีกเร็วๆนี้ !
(ภายในวันที่ 2017–11–20)(เพื่อเตือนตัวเอง)(และให้คนอื่นเตือน)

--

--