gc
เป็นการนำตัวรวบรวมขยะแบบอนุรักษ์นิยม เธรดโลคอล ทำเครื่องหมายและกวาด การใช้งานจัดให้มีการแทนที่การทำงานอย่างสมบูรณ์สำหรับการโทร POSIX malloc()
, calloc()
, realloc()
และ free()
มาตรฐาน
จุดเน้นของ gc
คือการนำเสนอการใช้งาน GC แบบทำเครื่องหมายและกวาดอย่างสะอาดตามแนวคิด โดยไม่ต้องเจาะลึกของการเพิ่มประสิทธิภาพเฉพาะสถาปัตยกรรมโดยเฉพาะ (ดู เช่น Boehm GC สำหรับการดำเนินการดังกล่าว) ควรเหมาะสมอย่างยิ่งสำหรับวัตถุประสงค์ในการเรียนรู้ และเปิดกว้างสำหรับการเพิ่มประสิทธิภาพทุกประเภท (ยินดีต้อนรับ PR)
แรงจูงใจดั้งเดิมสำหรับ gc
คือความปรารถนาของฉันที่จะเขียน LISP ของตัวเองในภาษา C ตั้งแต่เริ่มต้นทั้งหมด - และนั่นจำเป็นต้องมีการรวบรวมขยะ
งานนี้คงเป็นไปไม่ได้หากปราศจากความสามารถในการอ่านผลงานของผู้อื่น โดยเฉพาะอย่างยิ่ง Boehm GC, tgc ของ orangeduck (ซึ่งเป็นไปตามอุดมคติของการเป็นคนตัวเล็กและเรียบง่าย) และคู่มือการเก็บขยะ
gc
ไปใช้ $ git clone [email protected]:mkirchner/gc.git
$ cd gc
ในการคอมไพล์โดยใช้คอมไพเลอร์ clang
:
$ make test
หากต้องการใช้ GNU Compiler Collection (GCC):
$ make test CC=gcc
การทดสอบควรจะเสร็จสิ้นสำเร็จ หากต้องการสร้างรายงานความครอบคลุมปัจจุบัน:
$ make coverage
...
#include "gc.h"
...
void some_fun () {
...
int * my_array = gc_calloc ( & gc , 1024 , sizeof ( int ));
for ( size_t i = 0 ; i < 1024 ; ++ i ) {
my_array [ i ] = 42 ;
}
...
// look ma, no free!
}
int main ( int argc , char * argv []) {
gc_start ( & gc , & argc );
...
some_fun ();
...
gc_stop ( & gc );
return 0 ;
}
สิ่งนี้จะอธิบายเกี่ยวกับ API หลัก โปรดดูรายละเอียดเพิ่มเติมใน gc.h
และ API ระดับต่ำ
ในการเริ่มต้นและเริ่มการรวบรวมขยะ ให้ใช้ฟังก์ชัน gc_start()
และส่งที่อยู่ ด้านล่างสุดของสแต็ก :
void gc_start ( GarbageCollector * gc , void * bos );
bos
พารามิเตอร์ที่อยู่ด้านล่างสุดของสแต็กจำเป็นต้องชี้ไปที่ตัวแปรที่จัดสรรสแต็ก และทำเครื่องหมายจุดต่ำสุดของสแต็กจากจุดเริ่มต้นการค้นหารูท (การสแกน)
การเก็บขยะสามารถหยุด หยุดชั่วคราว และดำเนินการต่อได้
void gc_stop ( GarbageCollector * gc );
void gc_pause ( GarbageCollector * gc );
void gc_resume ( GarbageCollector * gc );
และสามารถกระตุ้นการเก็บขยะด้วยตนเองได้ด้วย
size_t gc_run ( GarbageCollector * gc );
gc
รองรับการจัดสรรหน่วยความจำสไตล์ malloc()
, calloc()
และ realloc()
ลายเซ็นฟังก์ชันที่เกี่ยวข้องจะเลียนแบบฟังก์ชัน POSIX (ยกเว้นว่าเราต้องส่งผ่านตัวรวบรวมขยะเป็นอาร์กิวเมนต์แรก):
void * gc_malloc ( GarbageCollector * gc , size_t size );
void * gc_calloc ( GarbageCollector * gc , size_t count , size_t size );
void * gc_realloc ( GarbageCollector * gc , void * ptr , size_t size );
เป็นไปได้ที่จะส่งพอยน์เตอร์ไปยังฟังก์ชัน destructor ผ่านทางอินเทอร์เฟซแบบขยาย:
void * dtor ( void * obj ) {
// do some cleanup work
obj -> parent -> deregister ();
obj -> db -> disconnect ()
...
// no need to free obj
}
...
SomeObject * obj = gc_malloc_ext ( gc , sizeof ( SomeObject ), dtor );
...
gc
รองรับการจัดสรรแบบคงที่ซึ่งเป็นขยะที่รวบรวมเฉพาะเมื่อ GC ปิดระบบผ่าน gc_stop()
เพียงใช้ฟังก์ชันตัวช่วยที่เหมาะสม:
void * gc_malloc_static ( GarbageCollector * gc , size_t size , void ( * dtor )( void * ));
การจัดสรรแบบคงที่คาดว่าจะมีตัวชี้ไปยังฟังก์ชันการสรุป เพียงตั้งค่าเป็น NULL
หากไม่จำเป็นต้องทำการสรุป
โปรดทราบว่าปัจจุบัน gc
ไม่รับประกันการเรียงลำดับเฉพาะเมื่อรวบรวมตัวแปรคงที่ หากจำเป็นต้องจัดสรร vars แบบคงที่ในลำดับเฉพาะ ผู้ใช้ควรเรียก gc_free()
กับตัวแปรเหล่านั้นในลำดับที่ต้องการก่อนที่จะเรียก gc_stop()
ดูด้านล่าง .
นอกจากนี้ยังสามารถทริกเกอร์การจัดสรรหน่วยความจำอย่างชัดเจนได้โดยใช้
void gc_free ( GarbageCollector * gc , void * ptr );
การเรียก gc_free()
รับประกันว่าจะ (a) สิ้นสุด/ทำลายวัตถุที่ชี้โดย ptr
หากมี และ (b) เพื่อเพิ่มหน่วยความจำที่ ptr
ชี้ไปที่โดยไม่คำนึงถึงกำหนดเวลาปัจจุบันสำหรับการรวบรวมขยะ และจะทำงานด้วยหาก GC ได้รับ หยุดชั่วคราวโดยใช้ gc_pause()
ด้านบน
gc
ยังมีการใช้งาน strdup()
ที่ส่งคืนสำเนาที่รวบรวมขยะ:
char * gc_strdup ( GarbageCollector * gc , const char * s );
แนวคิดพื้นฐานเบื้องหลังการรวบรวมขยะคือการทำให้วงจรการจัดสรรหน่วยความจำ/การจัดสรรคืนเป็นไปโดยอัตโนมัติ ซึ่งทำได้โดยการติดตามหน่วยความจำที่จัดสรรไว้ทั้งหมด และทริกเกอร์การจัดสรรคืนสำหรับหน่วยความจำที่ยังคงจัดสรรอยู่แต่ไม่สามารถเข้าถึงได้เป็นระยะๆ
ตัวรวบรวมขยะขั้นสูงจำนวนมากยังใช้แนวทางของตนเองในการจัดสรรหน่วยความจำ (เช่น แทนที่ malloc()
) ซึ่งมักจะช่วยให้พวกเขาจัดวางหน่วยความจำในลักษณะประหยัดพื้นที่มากขึ้นหรือเพื่อการเข้าถึงที่รวดเร็วขึ้น แต่มาพร้อมกับราคาของการใช้งานเฉพาะสถาปัตยกรรมและความซับซ้อนที่เพิ่มขึ้น gc
หลีกเลี่ยงปัญหาเหล่านี้โดยย้อนกลับไปใช้งาน POSIX *alloc()
และแยกการจัดการหน่วยความจำและข้อมูลเมตาการรวบรวมขยะออกจากกัน สิ่งนี้ทำให้ gc
เข้าใจง่ายกว่ามาก แต่แน่นอนว่ายังประหยัดพื้นที่และเวลาน้อยกว่าแนวทางที่ได้รับการปรับปรุงให้เหมาะสมที่สุดอีกด้วย
โครงสร้างข้อมูลหลักภายใน gc
คือแผนที่แฮชที่จับคู่ที่อยู่ของหน่วยความจำที่จัดสรรกับข้อมูลเมตาการรวบรวมขยะของหน่วยความจำนั้น:
รายการในแผนที่แฮชเป็นการจัดสรร จำลองด้วย struct
Allocation
:
typedef struct Allocation {
void * ptr ; // mem pointer
size_t size ; // allocated size in bytes
char tag ; // the tag for mark-and-sweep
void ( * dtor )( void * ); // destructor
struct Allocation * next ; // separate chaining
} Allocation ;
Allocation
แต่ละรายการจะมีตัวชี้ไปยังหน่วยความจำที่จัดสรร ขนาดของหน่วยความจำที่จัดสรรในตำแหน่งนั้น แท็กสำหรับการทำเครื่องหมายและกวาด (ดูด้านล่าง) ตัวชี้ทางเลือกไปยังฟังก์ชัน destructor และตัวชี้ไปยัง Allocation
ถัดไป ( สำหรับการต่อโซ่แยกกัน ดูด้านล่าง)
การจัดสรรจะถูกรวบรวมใน AllocationMap
typedef struct AllocationMap {
size_t capacity ;
size_t min_capacity ;
double downsize_factor ;
double upsize_factor ;
double sweep_factor ;
size_t sweep_limit ;
size_t size ;
Allocation * * allocs ;
} AllocationMap ;
ที่เมื่อใช้ร่วมกับชุดฟังก์ชัน static
ที่ภายใน gc.c
จะจัดเตรียมความหมายแผนที่แฮชสำหรับการนำ API สาธารณะไปใช้
AllocationMap
เป็นโครงสร้างข้อมูลส่วนกลางในโครงสร้าง GarbageCollector
ซึ่งเป็นส่วนหนึ่งของ API สาธารณะ:
typedef struct GarbageCollector {
struct AllocationMap * allocs ;
bool paused ;
void * bos ;
size_t min_size ;
} GarbageCollector ;
ด้วยโครงสร้างข้อมูลพื้นฐานที่มีอยู่แล้ว คำขอการจัดสรรหน่วยความจำ gc_*alloc()
ใดๆ จึงเป็นขั้นตอนสองขั้นตอน: ขั้นแรก จัดสรรหน่วยความจำผ่านฟังก์ชันระบบ (เช่น มาตรฐาน malloc()
) และประการที่สอง เพิ่มหรืออัปเดตข้อมูลเมตาที่เกี่ยวข้องไปยัง แผนที่แฮช
สำหรับ gc_free()
ให้ใช้พอยน์เตอร์เพื่อค้นหาข้อมูลเมตาในแมปแฮช ตรวจสอบว่าการจัดสรรคืนต้องมีการเรียกตัวทำลายหรือไม่ โทรหากจำเป็น เพิ่มหน่วยความจำที่ได้รับการจัดการ และลบรายการข้อมูลเมตาออกจากแมปแฮช
โครงสร้างข้อมูลเหล่านี้และอินเทอร์เฟซที่เกี่ยวข้องทำให้สามารถจัดการข้อมูลเมตาที่จำเป็นในการสร้างตัวรวบรวมขยะ
gc
ทริกเกอร์การรวบรวมภายใต้สองสถานการณ์: (ก) เมื่อการเรียกการจัดสรรระบบล้มเหลว (โดยหวังว่าจะจัดสรรหน่วยความจำที่เพียงพอเพื่อตอบสนองคำขอปัจจุบัน); และ (b) เมื่อจำนวนรายการในแผนที่แฮชผ่านเครื่องหมายน้ำสูงที่ปรับแบบไดนามิก
หากเกิดกรณีใดกรณีหนึ่งเหล่านี้ gc
จะหยุดโลกและเริ่มการรวบรวมขยะแบบทำเครื่องหมายแล้วกวาดซึ่งดำเนินการกับการจัดสรรปัจจุบันทั้งหมด ฟังก์ชันนี้ถูกนำมาใช้ในฟังก์ชัน gc_run()
ซึ่งเป็นส่วนหนึ่งของ API สาธารณะ และมอบหมายงานทั้งหมดให้กับฟังก์ชัน gc_mark()
และ gc_sweep()
ที่เป็นส่วนหนึ่งของ API ส่วนตัว
gc_mark()
มีหน้าที่ในการค้นหารูทและติดแท็กการจัดสรรที่รู้จักทั้งหมดที่อ้างอิงจากรูท (หรือจากการจัดสรรที่อ้างอิงจากรูท เช่น แบบส่งผ่าน) เป็น "ใช้แล้ว" เมื่อการทำเครื่องหมายของเสร็จสมบูรณ์ gc_sweep()
จะวนซ้ำการจัดสรรที่ทราบทั้งหมด และจัดสรรการจัดสรรที่ไม่ได้ใช้ (เช่น ไม่มีการทำเครื่องหมาย) ทั้งหมด กลับสู่ gc_run()
และโลกยังคงทำงานต่อไป
gc
จะเก็บการจัดสรรหน่วยความจำที่ สามารถเข้าถึงได้ และรวบรวมทุกสิ่งทุกอย่าง การจัดสรรจะถือว่าสามารถเข้าถึงได้หากข้อใดข้อหนึ่งต่อไปนี้เป็นจริง:
gc_start()
(กล่าวคือ bos
คือที่อยู่สแต็กที่เล็กที่สุดที่พิจารณาในระหว่างเฟสการทำเครื่องหมาย)gc_*alloc()
ซึ่งชี้ไปยังเนื้อหาการจัดสรรGC_TAG_ROOT
อัลกอริธึมการทำเครื่องหมายและกวาดแบบไร้เดียงสาทำงานในสองขั้นตอน ขั้นแรก ในขั้นตอน การทำเครื่องหมาย อัลกอริธึมจะค้นหาและทำเครื่องหมายการจัดสรร รูท ทั้งหมดและการจัดสรรทั้งหมดที่สามารถเข้าถึงได้จากรูท ประการที่สอง ในขั้นตอน การกวาด อัลกอริธึมจะส่งผ่านการจัดสรรที่ทราบทั้งหมด โดยรวบรวมการจัดสรรทั้งหมดที่ไม่ได้ทำเครื่องหมายไว้และถือว่าไม่สามารถเข้าถึงได้
ในตอนเริ่มต้นของขั้น ทำเครื่องหมาย ก่อนอื่นเราจะกวาดล้างการจัดสรรที่ทราบทั้งหมด และค้นหารากที่ชัดเจนด้วยชุดแท็ก GC_TAG_ROOT
แต่ละรากเหล่านี้เป็นจุดเริ่มต้นสำหรับการมาร์กแบบเรียกซ้ำเชิงลึกก่อน
gc
ตรวจพบรากทั้งหมดในสแต็กในเวลาต่อมา (เริ่มต้นจาก bos
ตัวชี้ด้านล่างของสแต็กที่ส่งผ่านไปยัง gc_start()
) และรีจิสเตอร์ (โดยการทิ้งมันลงบนสแต็กก่อนเฟสการทำเครื่องหมาย) และใช้สิ่งเหล่านี้เป็นจุดเริ่มต้นสำหรับ การทำเครื่องหมายเช่นกัน
ด้วยการจัดสรรราก การทำเครื่องหมายประกอบด้วย (1) การตั้งค่าฟิลด์ tag
ในออบเจ็กต์ Allocation
เป็น GC_TAG_MARK
และ (2) การสแกนหน่วยความจำที่จัดสรรเพื่อหาพอยน์เตอร์ไปยังการจัดสรรที่ทราบ โดยทำซ้ำกระบวนการซ้ำๆ
การใช้งานพื้นฐานคือการค้นหาเชิงลึกก่อนแบบเรียกซ้ำซึ่งจะสแกนเนื้อหาหน่วยความจำทั้งหมดเพื่อค้นหาการอ้างอิงที่เป็นไปได้:
void gc_mark_alloc ( GarbageCollector * gc , void * ptr )
{
Allocation * alloc = gc_allocation_map_get ( gc -> allocs , ptr );
if ( alloc && !( alloc -> tag & GC_TAG_MARK )) {
alloc -> tag |= GC_TAG_MARK ;
for ( char * p = ( char * ) alloc -> ptr ;
p < ( char * ) alloc -> ptr + alloc -> size ;
++ p ) {
gc_mark_alloc ( gc , * ( void * * ) p );
}
}
}
ใน gc.c
นั้น gc_mark()
เริ่มกระบวนการทำเครื่องหมายโดยการทำเครื่องหมายรูทที่รู้จักบนสแต็กผ่านการเรียกไปที่ gc_mark_roots()
เพื่อทำเครื่องหมายราก เราทำการส่งผ่านแบบเต็มผ่านการจัดสรรที่ทราบทั้งหมด จากนั้นเราดำเนินการถ่ายโอนข้อมูลการลงทะเบียนบนสแต็ก
เพื่อให้เนื้อหาการลงทะเบียน CPU พร้อมสำหรับการค้นหารูท gc
ทิ้งเนื้อหาเหล่านั้นลงบนสแต็ก สิ่งนี้ถูกนำไปใช้ในลักษณะพกพาได้โดยใช้ setjmp()
ซึ่งจัดเก็บไว้ในตัวแปร jmp_buf
ก่อนที่เราจะทำเครื่องหมายสแต็ก:
...
/* Dump registers onto stack and scan the stack */
void ( * volatile _mark_stack )( GarbageCollector * ) = gc_mark_stack ;
jmp_buf ctx ;
memset ( & ctx , 0 , sizeof ( jmp_buf ));
setjmp ( ctx );
_mark_stack ( gc );
...
ทางเบี่ยงโดยใช้ตัวชี้ฟังก์ชัน volatile
_mark_stack
ไปยังฟังก์ชัน gc_mark_stack()
เป็นสิ่งจำเป็นเพื่อหลีกเลี่ยงการอินไลน์ของการเรียกไปยัง gc_mark_stack()
หลังจากทำเครื่องหมายหน่วยความจำทั้งหมดที่สามารถเข้าถึงได้และอาจยังคงใช้งานอยู่ การรวบรวมการจัดสรรที่ไม่สามารถเข้าถึงได้จึงเป็นเรื่องเล็กน้อย นี่คือการใช้งานจาก gc_sweep()
:
size_t gc_sweep ( GarbageCollector * gc )
{
size_t total = 0 ;
for ( size_t i = 0 ; i < gc -> allocs -> capacity ; ++ i ) {
Allocation * chunk = gc -> allocs -> allocs [ i ];
Allocation * next = NULL ;
while ( chunk ) {
if ( chunk -> tag & GC_TAG_MARK ) {
/* unmark */
chunk -> tag &= ~ GC_TAG_MARK ;
chunk = chunk -> next ;
} else {
total += chunk -> size ;
if ( chunk -> dtor ) {
chunk -> dtor ( chunk -> ptr );
}
free ( chunk -> ptr );
next = chunk -> next ;
gc_allocation_map_remove ( gc -> allocs , chunk -> ptr , false);
chunk = next ;
}
}
}
gc_allocation_map_resize_to_fit ( gc -> allocs );
return total ;
}
เราวนซ้ำการจัดสรรทั้งหมดในแมปแฮช ( for
loop) ตามทุกเชน (ลูป while
ด้วย chunk = chunk->next
) และ (1) ยกเลิกการทำเครื่องหมาย chunk หากมันถูกทำเครื่องหมายไว้; หรือ (2) เรียกตัวทำลายล้างบนก้อนและปลดปล่อยหน่วยความจำหากไม่ได้ทำเครื่องหมายไว้ โดยจะรักษาจำนวนหน่วยความจำที่เราว่างทั้งหมดไว้
นั่นเป็นการสรุปการทำเครื่องหมายและการกวาดล้าง โลกที่หยุดนิ่งกลับมาอีกครั้ง และเราพร้อมสำหรับการวิ่งครั้งต่อไป!