Go 1.27 มีการจัดสรรหน่วยความจำที่เร็วขึ้นสำหรับการจัดสรร 80 ไบต์หรือน้อยกว่า การจัดสรรอาจเร็วขึ้นถึง 20-30% ทำให้โปรแกรมที่มีการจัดสรรหนักเร็วขึ้นถึง 1% รันไทม์ Go ปรับปรุงประสิทธิภาพของการจัดสรรเหล่านี้โดยการเพิ่มฟังก์ชันพิเศษที่ใช้ในการจัดสรรขนาดบางขนาด ฟังก์ชันพิเศษเหล่านี้สามารถตั้งสมมติฐานบางอย่างที่ทำให้การปรับให้เหมาะสมเร็วขึ้นและง่ายขึ้น โพสต์ในบล็อกนี้จะอธิบายวิธีการทำงานและทำให้โปรแกรมของคุณเร็วขึ้นได้อย่างไร
การจัดสรรฮีปถูกสร้างขึ้นโดยรันไทม์ mallocgc ซึ่งต้องการขนาดของการจัดสรรและมีพอยน์เตอร์หรือไม่ เมื่อคอมไพเลอร์พิจารณาว่าวัตถุหนีไปยังฮีปหรือจำเป็นต้องได้รับการจัดสรรแบบไดนามิก คอมไพเลอร์จะแทรกการเรียกไปที่ newobjectซึ่งเป็นฟังก์ชัน wrapper ธรรมดาที่แยกขนาดของวัตถุและดูว่าวัตถุนั้นมีพอยน์เตอร์และส่งผ่านไปหรือไม่ mallocgc.
ข้อมูลสองชิ้นนี้จะกำหนดงานส่วนใหญ่ที่ผู้จัดสรรต้องทำ ขนาดมีความสำคัญเนื่องจากตัวจัดสรรจะกำหนดช่วงของขนาดที่เรียกว่า “คลาสขนาด” สำหรับการจัดสรรส่วนใหญ่ที่ไม่ใหญ่หรือเล็กเกินไป ระบบจะส่งคืนบล็อกหน่วยความจำจากรายการอ็อบเจ็กต์ว่าง ซึ่งทุกขนาดจะเป็นขนาดสูงสุดของคลาสขนาด ตัวอย่างเช่น ขนาดคลาส 3 คือ 17 ถึง 24 ไบต์ ดังนั้น ไม่ว่าคุณจะจัดสรร 17 หรือ 24 ไบต์ ตัวจัดสรรจะจัดเตรียมวัตถุ 24 ไบต์ว่างถัดไปในรายการของวัตถุ 24 ไบต์ ด้านล่างนี้เป็นตารางช่วงขนาดสำหรับแต่ละคลาสขนาดสูงสุด 80 ไบต์ มีชุดรายการอิสระแยกกัน ซึ่งเราเรียกว่า spans ขึ้นอยู่กับว่าการจัดสรรมีพอยน์เตอร์หรือไม่ เนื่องจากการบัญชีที่เราต้องทำเพื่อคนเก็บขยะ
| ขนาดคลาส | ช่วงขนาด |
|---|---|
1 |
1-8 ไบต์ |
2 |
9-16 ไบต์ |
3 |
17-24 ไบต์ |
4 |
25-32 ไบต์ |
5 |
33-48 ไบต์ |
6 |
49-64 ไบต์ |
7 |
65-80 ไบต์ |
เนื่องจากคลาสขนาดและการจัดสรรมีพอยน์เตอร์เป็นตัวกำหนดช่วงที่เราจัดสรรหรือไม่ ตัวจัดสรรจึงมีประเภทที่เรียกว่าคลาสช่วง ซึ่งค่าจะเข้ารหัสทั้งคู่: มันถูกกำหนดเป็น sizeClass<<1 | noPointers. เมื่อนำ malloc พิเศษตามขนาดไปใช้ ก็เห็นได้ชัดว่าเหมาะสมกว่าที่จะเชี่ยวชาญเฉพาะในคลาส span เหล่านี้ เนื่องจากพฤติกรรมตัวจัดสรรส่วนใหญ่ถูกกำหนดโดยคลาส span
เราสร้างทีมงานที่เชี่ยวชาญ mallocgc ตัวแปรสำหรับแต่ละคลาส span ตัวอย่างเช่น ฟังก์ชันที่จัดสรรวัตถุ pointerless ในคลาสขนาด 3 เรียกว่า mallocgcSmallNoScanSC3.
Small แปลว่า ไม่เล็กหรือใหญ่, NoScan หมายความว่าไม่มีพอยน์เตอร์และ SC3 หมายถึงขนาดคลาส 3 (17-24 ไบต์) ฟังก์ชันพิเศษได้รับการออกแบบให้เรียบง่ายที่สุดเท่าที่จะเป็นไปได้ และไม่สามารถจัดการทุกกรณีในระหว่างการจัดสรรได้ เมื่อตรวจพบกรณีดังกล่าว เช่น เมื่อ GC ทำงานอยู่ พวกเขาจะหันไปใช้รูทีนการจัดสรรทั่วไปมากขึ้น
ดังนั้นเราจึงได้ผู้เชี่ยวชาญคนใหม่ mallocgc ฟังก์ชันสำหรับตัวพิมพ์เล็ก (ไม่มีตัวชี้เสมอ) และอีกหนึ่งตัวสำหรับแต่ละคลาสช่วงที่ไม่ใช่ตัวพิมพ์เล็ก: ช่วงแบบไม่มีตัวชี้ของขนาดคลาส 2 และสูงกว่า และช่วงตัวชี้ของขนาดคลาส 1 และสูงกว่า เฉพาะทางเหล่านี้ mallocgc ตัวแปรนั้นเร็วกว่าการโทร mallocgcแต่ประโยชน์ของความเชี่ยวชาญพิเศษจะลดลงเมื่อขนาดเพิ่มขึ้น การจัดสรรมักถูกครอบงำโดยความจำเป็นในการล้างหน่วยความจำ และเมื่อถึงจุดหนึ่ง งานการจัดสรรที่เหลือก็ไม่มีนัยสำคัญ และเร็วกว่านั้นไม่เพียงพอ
mallocgc! ด้วยการจัดสรรขนาดเฉพาะ เมื่อคอมไพลเลอร์รู้ว่าคลาสสแปนใดที่ต้องจัดสรรให้ คอมไพลเลอร์สามารถแทรกการเรียกไปยังฟังก์ชันพิเศษได้โดยตรง แทนที่จะเรียกไปที่
newobject. แต่คอมไพเลอร์มักจะไม่รู้ว่าคลาสสแปนใดที่ถูกจัดสรรเมื่อสร้างโค้ด: ลองนึกถึงการแบ่งส่วนความยาวแบบไดนามิก เนื่องจากคอมไพเลอร์ไม่สามารถระบุขนาดของการจัดสรรได้ จึงจะเก็บการเรียกไว้ mallocgcและ mallocgc เองก็ต้องพิจารณาว่ามีฟังก์ชั่นพิเศษอะไรหรือเปล่า แล้วต้องเรียกมันว่าอะไร ดังนั้นประสิทธิภาพของฟังก์ชันพิเศษจึงต้องสูงเพียงพอที่แม้จะมีค่าใช้จ่ายในการโทรแบบไดนามิก แต่ก็ยังเร็วขึ้น
ปัญหาการโอเวอร์โหลดนี้ไม่ใช่ปัญหาเดียวเท่านั้น หากเป็นเช่นนั้น เราสามารถสร้างฟังก์ชันพิเศษที่มีขนาดใหญ่ขึ้นและสงวนไว้ให้คอมไพเลอร์แทรกเมื่อทราบขนาด ณ เวลาคอมไพล์ และถ้าเราไม่เรียกมันแบบไดนามิก เราก็ไม่ต้องจ่ายค่าใช้จ่ายแบบไดนามิก แต่แต่ละฟังก์ชันพิเศษที่เพิ่มเข้ามาจะเพิ่มขนาดของไฟล์ปฏิบัติการที่สร้างโดยคอมไพเลอร์ และที่สำคัญกว่านั้นคือใช้พื้นที่แคชคำสั่งอันมีค่า คนโสด mallocgc โดยปกติฟังก์ชันนี้จะอยู่ในแคชคำสั่งเนื่องจากความถี่ในการจัดสรรเกิดขึ้น หากเรามีฟังก์ชันพิเศษมากมายและไม่มีอยู่ในแคช ค่าใช้จ่ายในการดึงรหัสพิเศษจากแคชอาจทำให้ผลประโยชน์ใดๆ หมดไป และยิ่งรหัสการจัดสรรมีความชำนาญมากขึ้นใน icache ยิ่งใช้รหัสผู้ใช้มากขึ้น ทำให้การค้นหาและเรียกใช้รหัสผู้ใช้ช้าลง การดำเนินการวัดประสิทธิภาพหลายครั้งโดยหยุดที่คลาสที่มีขนาดแตกต่างกัน เราพบว่าการหยุดที่ 80 ไบต์เป็นจุดที่เหมาะสม
แน่นอนว่าการเพิ่มฟังก์ชันพิเศษเพิ่มเติมหมายถึงโค้ดที่ต้องบำรุงรักษามากขึ้น หากแต่ละฟังก์ชันเขียนด้วยมือ รหัสในฟังก์ชันพิเศษอาจแยกออกจากกันและไม่ซิงค์กันได้ง่าย เพื่อช่วยในเรื่องนี้ เราได้แยกส่วนทั่วไปออกจากฟังก์ชันพิเศษ และเขียนอินไลน์โดยใช้ไลบรารีมาตรฐาน go/ast แพ็คเกจเพื่อแยกวิเคราะห์และจัดรูปแบบและ golang.org/x/tools/go/ast/astutil แพ็คเกจเพื่อจัดการ AST ส่วนทั่วไปของฟังก์ชันทั้งหมดเขียนด้วยโค้ด Go มาตรฐานที่สร้างขึ้นและตรวจสอบกับรันไทม์ที่เหลือเพื่อให้เครื่องมือของเราตรวจพบปัญหา แต่โดยส่วนใหญ่แล้วเป็นเพียงส่วนย่อยของอินไลน์
เราสามารถวัดการปรับปรุงในฟังก์ชันการจัดสรรขนาดพิเศษได้ แต่เหตุใดจึงเร็วกว่าจริง ๆ การเพิ่มประสิทธิภาพที่ชัดเจนที่สุดคือการทำความสะอาดหน่วยความจำ ความทรงจำกลับคืนมาโดย mallocgc
ไม่จำเป็นต้องรีเซ็ตเสมอไป แต่มักจะรีเซ็ต และอาจใช้เวลาส่วนใหญ่ในการจัดสรร ฟังก์ชั่นล้างหน่วยความจำ
memclrNoHeapPointers มันเขียนด้วยชุดประกอบที่ได้รับการปรับปรุงให้เหมาะสมที่สุด แต่เราสามารถทำได้ดีกว่าสำหรับพื้นที่ขนาดเล็ก ในฟังก์ชันพิเศษ ถ้าขนาดที่ชัดเจนเป็นค่าคงที่ คอมไพลเลอร์สามารถทดแทนการเรียกได้ memclrNoHeapPointers พร้อมรหัสเพื่อสร้างคำแนะนำในการล้างหน่วยความจำโดยตรง การจัดสรรอาจข้ามการเรียกใช้ฟังก์ชันและบางสาขา สิ่งนี้สร้างความแตกต่างให้กับการจัดสรรเพียงเล็กน้อย แต่เมื่อมีขนาดใหญ่ขึ้น ค่าใช้จ่ายในการเรียกใช้ฟังก์ชันก็จะกลายเป็นเรื่องเล็กน้อย
แม้ว่าการล้างหน่วยความจำได้เร็วขึ้นจะให้การปรับปรุงครั้งใหญ่ที่สุด แต่ก็มีเทคนิคอื่นๆ อีกสองสามอย่างที่เป็นไปได้ด้วยฟังก์ชันพิเศษ: เนื่องจากเป็นคุณสมบัติเฉพาะของคลาสสแปน ฟังก์ชันจึงไม่จำเป็นต้องคำนวณคลาสสแปนเมื่อดึงข้อมูลสแปน และเนื่องจากขนาดของการจัดสรรคงที่ คอมไพเลอร์จึงสามารถดำเนินการปรับให้เหมาะสมบางอย่างเพื่อเร่งความเร็วในการบัญชีที่จำเป็นสำหรับการจัดสรร กรณีหนึ่งคือการทำเครื่องหมายตำแหน่งที่พอยน์เตอร์อยู่ในหน่วยความจำที่จัดสรร ฟังก์ชันพิเศษยังสามารถรวมฟังก์ชันเสริมหลายอย่างด้วยตนเองได้ คอมไพเลอร์ Go สามารถฝังโค้ดได้ แต่หลีกเลี่ยงฟังก์ชันที่ถือว่าใหญ่เกินไป เราสามารถแทนที่สิ่งนี้ในโค้ดที่สร้างขึ้นได้โดยการแทรกส่วนของฟังก์ชันลงในผู้เรียก ด้วยเครื่องกำเนิด เราสามารถสร้างสำเนาของแต่ละเนื้อหาได้โดยไม่ต้องกังวลกับการเบี่ยงเบนของแต่ละสำเนา และเราสามารถย้ายโค้ดที่จัดการกรณีทั่วไปน้อยกว่า เช่น แฟล็กดีบักรันไทม์ เพื่อทำให้ฟังก์ชันพาธช้าลงเพื่อทำให้ฟังก์ชันพิเศษบางลง
แม้ว่าเราหวังว่าคำอธิบายเกี่ยวกับการจัดสรรขนาดเฉพาะนี้น่าสนใจ แต่คุณในฐานะโปรแกรมเมอร์ Go ไม่จำเป็นต้องคิดถึงเรื่องนี้เมื่อเขียนโค้ด การจัดสรรหน่วยความจำจะเร็วขึ้นเล็กน้อย โดยประโยชน์สูงสุดจะอยู่ที่ขนาดการจัดสรรทั่วไปบางขนาด โดยเฉพาะอย่างยิ่งการจัดสรร 16 และ 24 ไบต์ การจัดสรรเหล่านี้เป็นบางส่วนที่พบบ่อยที่สุดเนื่องจากประกอบด้วยค่า 64 บิตสองหรือสามค่า ดังนั้นจึงรวมการจัดสรรสำหรับสิ่งต่าง ๆ เช่นค่าอินเทอร์เฟซและสตริงที่มีค่าสองค่าหรือส่วนที่มีสามส่วน เราใช้เวลามากมายในการปรับแต่งพฤติกรรมของ malloc เฉพาะขนาดและรับรองว่าผลกระทบของแคชคำสั่งมีน้อยมาก เดิมทีเราวางแผนจะปล่อยการจัดสรรขนาดพิเศษใน Go 1.26 แต่ตัดสินใจที่จะรอการเปิดตัวเพิ่มเติมเพื่อทำการปรับแต่งเพิ่มเติมและลดขนาดโค้ดเพิ่มเติมให้มากที่สุด
สิ่งที่คุณต้องทำเพื่อให้ได้ประสิทธิภาพที่ดีขึ้นจากการจัดสรรขนาดเฉพาะในโปรแกรมของคุณคือสร้างด้วย Go 1.27 หากคุณสนใจการดำเนินการที่เป็นรูปธรรมเพิ่มเติมที่คุณสามารถทำได้เพื่อปรับปรุงการจัดสรรหน่วยความจำและประสิทธิภาพการรวบรวมขยะ โปรดอ่านคู่มือการเพิ่มประสิทธิภาพ Go Garbage Collector
แม้ว่าเราจะมั่นใจว่าการจัดสรรขนาดแบบพิเศษไม่ควรทำให้เกิดการถดถอยในโค้ดของคุณ แต่หากจำเป็น คุณสามารถสร้างโปรแกรมโดยใช้ GOEXPERIMENT=nosizespecializedmalloc เพื่อปิดการใช้งาน หากคุณจำเป็นต้องทำเช่นนี้เพื่อแก้ไขปัญหาที่คุณประสบกับการจัดสรรขนาดแบบพิเศษ โปรดแจ้งปัญหาที่ go.dev/issue/new เพื่อให้เราตรวจสอบได้