-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fail to collect references in release build #259
Comments
welp i figured out a way, tho it is not scalable mps_root_t static_stack_root;
mps_addr_t static_stack[200];
int static_stack_counter; mps_res_t res = mps_reserve(&state->static_stack[state->static_stack_counter], state->ap, size);
if (res != MPS_RES_OK) managed_obj_error("out of memory in make_pointer");
// printf("reserved pointer %p\n", static_stack[static_stack_counter]);
obj = (managed_obj_t)state->static_stack[state->static_stack_counter];
obj->pointer.type = MANAGED_OBJECT_TYPE_POINTER;
obj->pointer.pointer = pointer;
// printf("pointer %p contains pointer %p\n", obj, obj->pointer.pointer);
} while(!mps_commit(state->ap, state->static_stack[state->static_stack_counter], size));
state->static_stack_counter++;
if (state->static_stack_counter == 200)
state->static_stack_counter = 0; /* Create an MPS arena. There is usually only one of these in a process.
It holds all the MPS "global" state and is where everything happens. */
MPS_ARGS_BEGIN(args) {
MPS_ARGS_ADD(args, MPS_KEY_ARENA_SIZE, arena_size);
res = mps_arena_create_k(&state->arena, mps_arena_class_vm(), args);
} MPS_ARGS_END(args);
if (res != MPS_RES_OK) managed_obj_error("Couldn't create arena");
// ...
res = mps_root_create_area(&state->static_stack_root, state->arena, mps_rank_exact(), 0,
state->static_stack, state->static_stack + 200, mps_scan_area, NULL);
if(res != MPS_RES_OK) managed_obj_error("Couldn't register stack root"); void managed_obj_deinit(ManagedObjState * state) {
printf("deinit\n");
memset(state->static_stack, 0, sizeof(mps_addr_t)*200);
printf("deinit - collect\n");
managed_obj_collect(state);
printf("deinit - shutdown\n");
mps_arena_park(state->arena); /* ensure no collection is running */
mps_root_destroy(state->static_stack_root); /* destroy static stack root */
// mps_root_destroy(state->stack_root); /* destroy stack root before thread */
// mps_thread_dereg(state->thread); /* deregister thread before ap */
mps_ap_destroy(state->ap); /* destroy ap before pool */
mps_pool_destroy(state->pool); /* destroy pool before fmt */
mps_fmt_destroy(state->fmt); /* destroy fmt before chain */
mps_chain_destroy(state->chain); /* destroy chain before arena */
mps_arena_destroy(state->arena); /* last of all */
memset(state, 0, sizeof(ManagedObjState));
} |
is there a way we can make this more scalable to larger amounts of allocations? |
how feasible would it be to just register and destroy gc root structures as they are passed around? like 1 root for each gc allocation being tracked |
Hello! We've look at your code, and it has a problem with the registration of the stack root. Your are registering the stack from within a function, and using that function's stack frame as the cold end of the stack. See https://gist.github.com/mgood7123/8a25971c90faa61397701361a0dd8be5#file-managed_object_obj-c-L434 This means that none of the functions calling managed_obj_make will have their stack frames included in the root, and objects they point to might be GC'd by the MPS. The documentation section Thread Roots describes how the stack root must be created. All functions that have roots in registers must be called by the function that locates the cold end in the way shown. In general, you do not need to create and destroy roots very often -- certainly not once per object. The MPS is designed so that you register roots, then store pointers to objects you want to keep alive in those roots. The typical case is that the roots are the stack and registers of your thread, and all objects (recursively) reachable from there are kept alive. Preserving objects is just a matter of storing pointers to memory. You definitely could have a kind of C++ "root object" that had a call to mps_root_create in its constructor and mps_root_destroy in its destructor, but that's the sort of thing we expect you do rarely: typically only once per process run. We can't quite see from your code what you're trying to achieve. Feel free to explain your application. We're curious and would like to help. Also we recommend you study the Scheme Example to see how it creates roots. There might be a misunderstanding of what we call a "root". See https://memory-pool-system.readthedocs.io/en/version-1.118/glossary/r.html#term-root . A root is not GC'd by the MPS. A root is a (set of) pointers to objects managed by the MPS. So your original question "how to keep a root alive" might be trying to ask "how to keep an object referenced by a root alive"? The answer is that the MPS preserves all objects referenced by roots. If an object is dying, it is not referenced by a root. That could be because of the cold end problem I mentioned above. Thanks. |
i tried using the stack bottom ( which works correctly in debug build but fails in a release build (no GC takes place) res = mps_thread_reg(&state->thread, state->arena);
if (res != MPS_RES_OK) managed_obj_error("Couldn't register thread");
struct MANAGED_STACK_ADDRESS_BOEHM_GC_stack_base sb_end;
memset(&sb_end, 0, sizeof(struct MANAGED_STACK_ADDRESS_BOEHM_GC_stack_base));
int gcres = MANAGED_STACK_ADDRESS_BOEHM_GC_get_stack_base(&sb_end);
if (gcres == MANAGED_STACK_ADDRESS_BOEHM_GC_SUCCESS) {
} else if (gcres == MANAGED_STACK_ADDRESS_BOEHM_GC_UNIMPLEMENTED) {
managed_obj_error("GC_get_stack_base(start) unimplemented\n");
} else {
managed_obj_error("GC_get_stack_base(start) failed: %d\n", gcres);
}
printf("thread stack bottom: %p\n", sb_end.mem_base);
// res = mps_root_create_thread(&state->thread_stack_root, state->arena, state->thread, (void*)sb_end.mem_base);
// if (res != MPS_RES_OK) managed_obj_error("Couldn't create stack root");
res = mps_root_create_reg(&state->thread_stack_root, state->arena, mps_rank_ambig(), 0, state->thread, mps_stack_scan_ambig, sb_end.mem_base, 0);
if (res != MPS_RES_OK) printf("Couldn't create root");
|
ultimately i would like to create an api with MPS that allows me to basically have a stateful GC object that can "attach" and "detach" from threads for example
|
i tried ManagedObjState state;
managed_obj_init(&state, MANAGED_OBJECT_DEFAULT_ARENA_SIZE);
void managed_obj_init(ManagedObjState * state, size_t arena_size) {
if (state == NULL) {
managed_obj_error("state cannot be null");
}
memset(state, 0, sizeof(ManagedObjState));
mps_res_t res;
// ...
/*
A risk of using tagged pointers in registers and on the stack
is that in some circumstances, an optimizing compiler might
optimize away the tagged pointer, keeping only the untagged
version of the pointer. In this situation the pointer would be
ignored and if it was the last reference to the object the MPS
might incorrectly determine that it was dead.
#. Choose to tag pointers with zero, setting ``scan_area`` to
:c:func:`mps_scan_area_tagged` and setting ``pattern`` to
zero.
*/
void *marker = state;
res = mps_root_create_thread_tagged(
&state->thread_stack_root, state->arena,
mps_rank_ambig(), (mps_rm_t)0,
state->thread, mps_scan_area_tagged,
sizeof(mps_word_t) - 1, (mps_word_t)0,
marker
);
if (res != MPS_RES_OK) printf("Couldn't create root");
} but release build still fails to find the pointers where a debug build would successfully find the pointers void managed_obj_init(ManagedObjState * state, size_t arena_size) {
if (state == NULL) {
managed_obj_error("state cannot be null");
}
memset(state, 0, sizeof(ManagedObjState));
mps_res_t res;
MPS_ARGS_BEGIN(args) {
MPS_ARGS_ADD(args, MPS_KEY_ARENA_SIZE, arena_size);
res = mps_arena_create_k(&state->arena, mps_arena_class_vm(), args);
} MPS_ARGS_END(args);
if (res != MPS_RES_OK) managed_obj_error("Couldn't create arena");
/* Make sure we can pick up finalization messages. */
mps_message_type_enable(state->arena, mps_message_type_finalization());
MPS_ARGS_BEGIN(args) {
MPS_ARGS_ADD(args, MPS_KEY_FMT_ALIGN, MANAGED_OBJECT_ALIGNMENT);
MPS_ARGS_ADD(args, MPS_KEY_FMT_SCAN, managed_obj_scan);
MPS_ARGS_ADD(args, MPS_KEY_FMT_SKIP, managed_obj_skip);
MPS_ARGS_ADD(args, MPS_KEY_FMT_FWD, managed_obj_fwd);
MPS_ARGS_ADD(args, MPS_KEY_FMT_ISFWD, managed_obj_isfwd);
MPS_ARGS_ADD(args, MPS_KEY_FMT_PAD, managed_obj_pad);
res = mps_fmt_create_k(&state->fmt, state->arena, args);
} MPS_ARGS_END(args);
if (res != MPS_RES_OK) managed_obj_error("Couldn't create obj format");
res = mps_chain_create(&state->chain,
state->arena,
(sizeof(managed_obj_gen_params) / sizeof(managed_obj_gen_params[0])),
managed_obj_gen_params);
if (res != MPS_RES_OK) managed_obj_error("Couldn't create obj chain");
MPS_ARGS_BEGIN(args) {
MPS_ARGS_ADD(args, MPS_KEY_CHAIN, state->chain);
MPS_ARGS_ADD(args, MPS_KEY_FORMAT, state->fmt);
res = mps_pool_create_k(&state->pool, state->arena, mps_class_amc(), args);
} MPS_ARGS_END(args);
if (res != MPS_RES_OK) managed_obj_error("Couldn't create obj pool");
res = mps_ap_create_k(&state->ap, state->pool, mps_args_none);
if (res != MPS_RES_OK) managed_obj_error("Couldn't create obj allocation point");
void *marker = state;
res = mps_root_create_thread_tagged(
&state->thread_stack_root, state->arena,
mps_rank_ambig(), (mps_rm_t)0,
state->thread, mps_scan_area_tagged,
sizeof(mps_word_t) - 1, (mps_word_t)0,
marker
);
if (res != MPS_RES_OK) printf("Couldn't create root");
} managed_obj_t managed_obj_make(ManagedObjState * state, void * pointer)
{
mps_addr_t port_ref;
managed_obj_t obj;
size_t size = MANAGED_OBJECT_ALIGN_OBJ(sizeof(managed_obj_pointer_s));
do {
mps_res_t res = mps_reserve(&state->static_stack[state->static_stack_counter], state->ap, size);
if (res != MPS_RES_OK) managed_obj_error("out of memory in make_pointer");
obj = (managed_obj_t)state->static_stack[state->static_stack_counter];
obj->pointer.type = MANAGED_OBJECT_TYPE_POINTER;
obj->pointer.pointer = pointer;
} while(!mps_commit(state->ap, state->static_stack[state->static_stack_counter], size));
printf("reserved and comitted object %p with pointer %p\n", obj, obj->pointer.pointer);
state->static_stack_counter++;
if (state->static_stack_counter == 200)
state->static_stack_counter = 0;
state->total += sizeof(managed_obj_pointer_s);
port_ref = obj;
mps_finalize(state->arena, &port_ref);
return obj;
} void managed_obj_collect(ManagedObjState * state) {
unsigned long collect_start = mps_clock();
mps_arena_collect(state->arena);
managed_obj_mps_chat(state);
unsigned long collect_end = mps_clock();
ManagedObjTime collect_duration_time = managed_obj_convert_to_time(collect_end - collect_start);
printf("Total Collection duration: %d seconds, %d milliseconds, %d microseconds\n", collect_duration_time.seconds, collect_duration_time.milliseconds, collect_duration_time.microseconds);
} void managed_obj_deinit(ManagedObjState * state) {
printf("deinit\n");
memset(state->static_stack, 0, sizeof(mps_addr_t)*200);
printf("deinit - collect after memset\n");
managed_obj_collect(state);
printf("deinit - collected after memset\n");
printf("deinit - shutdown\n");
mps_arena_park(state->arena); /* ensure no collection is running */
mps_root_destroy(state->thread_stack_root); /* destroy stack root before thread */
mps_thread_dereg(state->thread); /* deregister thread before ap */
mps_ap_destroy(state->ap); /* destroy ap before pool */
mps_pool_destroy(state->pool); /* destroy pool before fmt */
mps_fmt_destroy(state->fmt); /* destroy fmt before chain */
mps_chain_destroy(state->chain); /* destroy chain before arena */
mps_arena_destroy(state->arena); /* last of all */
memset(state, 0, sizeof(ManagedObjState));
} TEST(MPS_collect2, 1) {
ManagedObjState state;
managed_obj_init(&state, MANAGED_OBJECT_DEFAULT_ARENA_SIZE);
managed_obj_t p1 = managed_obj_make(&state, malloc(500));
managed_obj_t p2 = managed_obj_make(&state, malloc(500));
printf("p1 is %p, &p = %p\n", p1, &p1);
printf("p2 is %p, &p = %p\n", p2, &p2);
printf("first collect, p should not be collected\n");
managed_obj_collect(&state); // should not collect p
printf("if p has been collected then we have not managed to track it correctly\n");
p1 = (managed_obj_t) 0x6; // no longer referenced
p2 = (managed_obj_t) 0x6; // no longer referenced
printf("second collect, p should be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("if p has NOT been collected then we have not managed to track it correctly\n");
managed_obj_deinit(&state);
} |
if we build using in clang/clang++ this seems to happen at using https://github.com/Ravenbrook/mps/master clang and clang++ testBuilder_add_include(gc_mps mps_master/code)
testBuilder_add_include(gc_mps include)
testBuilder_add_source(gc_mps mps_master/code/mps.c)
testBuilder_add_source(gc_mps src/managed_object_obj.c)
# CONFIG_VAR_COOL
# CONFIG_VAR_HOT
# CONFIG_VAR_RASH
testBuilder_add_compile_option(gc_mps "SHELL:-DCONFIG_VAR_COOL")
testBuilder_build_static_library(gc_mps) valgrind output (debug) https://gist.github.com/mgood7123/a565343e2f0f26c2f49ca6849632ab5c valgrind output (release) https://gist.github.com/mgood7123/008d9dd435cf8660dd305449ceec5331 debug build, variant COLD [ RUN ] MPS_collect2.1
reserved and comitted object 0x7fc3f691a000 with pointer 0xd7c1c0
reserved and comitted object 0x7fc3f691a018 with pointer 0xd7c3c0
p1 is 0x7fc3f691a000, &p = 0x7fff73cc7228
p2 is 0x7fc3f691a018, &p = 0x7fff73cc7220
first collect, p should not be collected
Total Collection duration: 0 seconds, 0 milliseconds, 554 microseconds
if p has been collected then we have not managed to track it correctly
second collect, p should be collected
object 0x7fc3f6980000 with pointer 0xd7c1c0 is being freed.
object 0x7fc3f6980018 with pointer 0xd7c3c0 is being freed.
Total Collection duration: 0 seconds, 1 milliseconds, 557 microseconds
if p has NOT been collected then we have not managed to track it correctly
deinit
deinit - collect after memset
Total Collection duration: 0 seconds, 0 milliseconds, 634 microseconds
deinit - collected after memset
deinit - shutdown
[ OK ] MPS_collect2.1 (16 ms) release build, variant COLD [ RUN ] MPS_collect2.1
reserved and comitted object 0x7f4da271a000 with pointer 0x1b911c0
reserved and comitted object 0x7f4da271a018 with pointer 0x1b913c0
p1 is 0x7f4da271a000, &p = 0x7fff2f5e4700
p2 is 0x7f4da271a018, &p = 0x7fff2f5e46f8
first collect, p should not be collected
Total Collection duration: 0 seconds, 0 milliseconds, 83 microseconds
if p has been collected then we have not managed to track it correctly
second collect, p should be collected
Total Collection duration: 0 seconds, 0 milliseconds, 29 microseconds
if p has NOT been collected then we have not managed to track it correctly
deinit
deinit - collect after memset
Total Collection duration: 0 seconds, 0 milliseconds, 58 microseconds
deinit - collected after memset
deinit - shutdown
[ OK ] MPS_collect2.1 (1 ms) |
if i make my pointer volatile managed_obj_t p1 = managed_obj_make(&state, malloc(500));
volatile managed_obj_t p2 = managed_obj_make(&state, malloc(500));
// ...
p1 = (managed_obj_t) 0x6; // no longer referenced
p2 = (managed_obj_t) 0x6; // no longer referenced
// ...
printf("second collect, p should be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p |
this seems like with optimizations, attempting to bring a variable into the local stack of a function MAY bind it to the frame, resulting in it being uncollectable until the frame ends this in turn CAN be troublesome if we attempt to init and destroy the GC in the same frame, since the GC cannot reclaim a variable that is bound to the same frame as it (or even may leak to a parent frame variable) |
one solution would be to forcefully collect all references before the arena is destroyed (regardless of weather such are reachable or not) but i do not know how to do this |
it seems we can do so via static Res rootDestroy(Root root, void *p)
{
/* we are inside a locked arena, we cannot */
/* use 'mps_root_destroy' since it locks the arena */
AVERT(Root, root);
Arena arena;
arena = RootArena(root);
RootDestroy(root);
return ResOK;
}
struct mps_thr_s;
// all mps ThreadStruct contain this layout
typedef struct mps_thr_s { /* ANSI fake thread structure */
Sig sig; /* design.mps.sig.field */
Serial serial; /* from arena->threadSerial */
Arena arena; /* owning arena */
RingStruct arenaRing; /* attaches to arena */
/* extra data per impl */
} ThreadStruct;
void destroy_roots(mps_arena_t mps_arena) {
Arena arena = (Arena)mps_arena;
/* lock the arena to prevent any roots from */
/* being created while we are iterating them */
ArenaEnter(arena);
AVER(ArenaGlobals(arena)->clamped); /* .assume.parked */
AVER(arena->busyTraces == TraceSetEMPTY); /* .assume.parked */
Globals arenaGlobals = ArenaGlobals(arena);
AVERT(Globals, arenaGlobals);
RootsIterate(arenaGlobals, rootDestroy, 0);
/* unregister all registered threads since */
/* we have no roots associated with them */
/*
_`.req.register.multi`: It must be possible to register the same
thread multiple times. (This is needed to support the situation where
a program that does not use the MPS is calling into MPS-using code
from multiple threads. On entry to the MPS-using code, the thread can
be registered, but it may not be possible to ensure that the thread is
deregistered on exit, because control may be transferred by some
non-local mechanism such as an exception or ``longjmp()``. We don't
want to insist that the client program keep a table of threads it has
registered, because maintaining the table might require allocation,
which might provoke a collection. See request.dylan.160252_.)
* Register/Deregister
*
* Explicitly register/deregister a thread on the arena threadRing.
* Register returns a "Thread" value which needs to be used
* for deregistration.
*
* Threads must not be multiply registered in the same arena.
*/
Ring threadRing = ArenaThreadRing(arena);
Ring node, next;
AVERT(Ring, threadRing);
RING_FOR(node, threadRing, next) {
Thread thread = RING_ELT(Thread, arenaRing, node);
/* we are inside a locked arena, we cannot */
/* use 'mps_thread_unreg' since it locks the arena */
AVER(ThreadCheckSimple(thread));
AVER(arena == ThreadArena(thread)); /* is the thread arena always us? */
ThreadDeregister(thread, arena);
}
ArenaLeave(arena);
} and then collect the arena mps_arena_park(state->arena); /* ensure no collection is running */
printf("deinit - collect\n");
destroy_roots(state->arena); // destroy all roots and unregister all threads from the given arena
managed_obj_collect(state); // mps_arena_collect + managed_obj_mps_chat
printf("deinit - collected\n");
printf("deinit - shutdown\n");
mps_ap_destroy(state->ap); /* destroy ap before pool */
mps_pool_destroy(state->pool); /* destroy pool before fmt */
mps_fmt_destroy(state->fmt); /* destroy fmt before chain */
mps_chain_destroy(state->chain); /* destroy chain before arena */
mps_arena_destroy(state->arena); /* last of all */ |
in a release build (with variant COOL) we get this ManagedObjState state;
managed_obj_init(&state, MANAGED_OBJECT_DEFAULT_ARENA_SIZE);
managed_obj_t p1 = managed_obj_make(&state, malloc(500));
managed_obj_t p2 = managed_obj_make(&state, malloc(500));
p1 = (managed_obj_t) 0x6; // no longer referenced
p2 = (managed_obj_t) 0x6; // no longer referenced
managed_obj_deinit(&state); [ RUN ] MPS_collect2.1
reserved and comitted object 0x7f98a3580000 with pointer 0x6991c0
reserved and comitted object 0x7f98a3580018 with pointer 0x6993c0
deinit - collect
object 0x7f98a3600000 with pointer 0x6991c0 is being freed.
object 0x7f98a3600018 with pointer 0x6993c0 is being freed.
Total Collection duration: 0 seconds, 0 milliseconds, 327 microseconds
deinit - collected
deinit - shutdown
[ OK ] MPS_collect2.1 (1 ms) |
do note this is intended to forcefully collect all references held by an arena, with the intention of destroying the arena afterwards
this means that any references (similar to freeing a memory arena and all pointers to such memory becoming UB (use-after-free) ) these should be detectable with debuggers and sanitizers |
Hello again! We are very interested in what you are doing, but our resource has been limited for the past few days. We will catch up. I'll just point out that there is no communication between GCs in separate arenas. Each arena is like a separate world. Each client program (with its own object graph) is intended to have a single arena. The use case for multiple arenas would be if there were, e.g. two separate language runtimes sharing a process, both linking to the MPS as a library, and calling each other via some sort of FFI mechanism. Pools within an arena are fully co-operative. The object graph can span pools and this will be handled correctly. |
so arena's eg, a reference assuming this to be the case, if an arena is being destroyed, how might we go about |
incedentally this MAY be an alternative to |
Arenas can contain anything, but the MPS GC will not automatically find references (pointers) from one arena to another. There is nothing to stop you creating roots in one arena for another arena, just like you would create roots from any other memory. The arenas do not see each other: they are separate worlds.
Please explain what you mean by "owned" here.
Please explain what you mean by "migrate" here. You can copy memory from one arena to another, e.g. with When dealing with GC finalization, it's very important to realise that there is no guarantee of promptness. See Cautions. Finalization by the GC is provided as an optimisation, and should not be used as a necessary part of an algorithm.
This is an interesting point. We don't currently guarantee that deleting all roots and collecting the world will recycle (and therefore finalize) every object, but we might be able to make that promise. We'll consider it. Also, internally, finalization is managed by a "guardian pool" that has a list of all remaining finalizable objects. It may be possible to provide an interface to that list, so that a program can examine them all. |
There may be some misunderstanding between us here. The MPS GC does not collect references (pointers to GC'd objects). It collects objects (blocks of memory). Objects can contain references, discovered by scanning. |
never mind, i realized that arena's give out references, and they cannot "take ownership" of references eg
say we have while this may contradict the following
a reference this
yea, assuming we have by deleting the roots themselves we basically force
true, however our goal is to |
Are you saying ref_1 is a pointer inside an object allocated in arena_1? The MPS will not examine references in arena_1 when it GCs arena_2. Arenas do not communicate. They are independent, separate worlds. So ref_1 is not ever keeping anything in arena_2 alive. The only way that arena_2 will know about ref_1 (which is outside arena_2) is if you create a root in arena_2 that includes ref_1. From your questions here, and in #260 , you seem to be focussed on the management of external resources (file handles, etc.), not on the management of memory blocks. Is that correct? Are you trying to use the MPS mainly to manage external resources automatically? |
I'll also just repeat that the MPS is designed to have one arena per program. The only use case for multiple arenas is if you have linked two independent programs (e.g. written by different organizations) into the same process, and they are sharing address space, but working independently. |
You are however welcome (and intended) to create multiple pools within your arena. That might be what you need. Let's say you create two instances of the AMC pool class, amcA and amcB. Now, if an object in amc1 (objA) is keeping an object in amc2 (objB) alive, and you destroy pool amc1, then objB will (eventually) be recycled (and possibly finalized). If you don't want that to happen, you need a pointer to objB in an object that is still alive, or in a root. How you do that is up to you, but the simplest case is that you just do |
The reason I ask about external resources is that the MPS makes decisions about collection based on factors to do with space and time: how much memory is available, CPU time available, and so on. It is designed to manage memory in a way that the running program will not notice. It's very good at keeping pause times down for interactive applications. It does not think about external resources or make plans based on them. If you're trying to clean up external resources (e.g. file handles) promptly you're probably using the wrong tool. Finalization is only an optimization that the MPS offers in passing. You should not rely on finalization messages. |
additionally, since all roots are destroyed, reference resurrection should become effectively impossible the |
yes but to actually perform a collection, all threads registered to such arena (excluding the thead running the collection) must be suspended for the duration of the collection, right? eg
when collect is ran, the this can in-turn be applied on a parallel basis
|
alright :) |
so in regards to references, |
assuming so, in regards to topology #260 and also actually keeping references alive in other references we would need some way to add |
No, not quite. During some parts of a collection, all threads that might read or write any reference (usually all threads in your process) must be suspended, so that we can atomically update the graph of all objects in the process. It's not safe to have some threads registered to one arena, and some to another arena, if there is any chance that the threads will look at or update any object, or share references with each other. This is because the GC must act globally on the entire state of the program. The details are very subtle. But your process and its threads are not stopped for the duration of the GC, only for short periods. The MPS is incremental and at quite a fine resolution. That is one reason it is successfully commercially: it does not interrupt the user even when there is a lot of GC to do. |
I'm out of time for today but I hope to help you again soon. |
would it be possible to limit such to only registered threads? |
alright |
We must suspend all threads that can look at any object managed by the MPS. Basically because we are going to move them around without the program noticing. |
so i assume we must explicitly register threads because we have no api to automatically register all existing/new threads? |
The amount of time that the threads are suspended is kept short. The MPS uses barriers so that it can GC and move objects without the threads noticing, even while they are running. |
hmm ok |
We don't currently have any portable code to do this. We could possibly add some convenience code by looking at how, e.g. the BDWGC finds threads. It's much more "drop-in" than the MPS currently. |
so a GC registers threads as roots to scan if a collect happens, ALL threads (both registered and unregistered) are the more threads get registered, the more we need to scan multiple pools can be used to seperate references, but |
eg /* ThreadRingSuspend -- suspend all threads on a ring, except the
* current one.
*/
static Bool threadSuspend(Thread thread)
{
Res res;
pthread_t self;
self = pthread_self();
if (pthread_equal(self, thread->id)) /* .thread.id */
return TRUE;
/* .error.suspend: if PThreadextSuspend fails, we assume the thread
* has been terminated. */
AVER(thread->context == NULL);
res = PThreadextSuspend(&thread->thrextStruct, &thread->context);
AVER(res == ResOK);
AVER(thread->context != NULL);
/* design.thread-manager.sol.thread.term.attempt */
return res == ResOK;
} /* PThreadextSuspend -- suspend a thread
*
* <design/pthreadext#.impl.suspend>
*/
Res PThreadextSuspend(PThreadext target, MutatorContext *contextReturn)
{
Ring node, next;
Res res;
int status;
AVERT(PThreadext, target);
AVER(contextReturn != NULL);
AVER(target->context == NULL); /* multiple suspends illegal */
/* Serialize access to suspend, makes life easier */
status = pthread_mutex_lock(&pthreadextMut);
AVER(status == 0);
AVER(suspendingVictim == NULL);
/* Threads are added to the suspended ring on suspension */
/* If the same thread Id has already been suspended, then */
/* don't signal the thread, just add the target onto the id ring */
RING_FOR(node, &suspendedRing, next) {
PThreadext alreadySusp = RING_ELT(PThreadext, threadRing, node);
if (alreadySusp->id == target->id) {
RingAppend(&alreadySusp->idRing, &target->idRing);
target->context = alreadySusp->context;
goto noteSuspended;
}
}
/* Ok, we really need to suspend this thread. */
suspendingVictim = target;
status = pthread_kill(target->id, PTHREADEXT_SIGSUSPEND);
if (status != 0) {
res = ResFAIL;
goto unlock;
}
/* Wait for the victim to acknowledge suspension. */
while (sem_wait(&pthreadextSem) != 0) {
if (errno != EINTR) {
res = ResFAIL;
goto unlock;
}
}
noteSuspended:
AVER(target->context != NULL);
RingAppend(&suspendedRing, &target->threadRing);
*contextReturn = target->context;
res = ResOK;
unlock:
suspendingVictim = NULL;
status = pthread_mutex_unlock(&pthreadextMut);
AVER(status == 0);
return res;
} |
Not quite. The client program (your code) registers threads with the arena. This allows the GC to suspend them. It does not create roots. The MPS does not automatically find threads. Only registered threads are suspended. However, if an unregistered thread accesses any managed objects in the arena or any references to those objects from roots, you could get heap corruption. The corruption could occur much later and will be very difficult to debug. So we would recommend you register all threads. There are some circumstances (special debugging hacks usually) where you might want an unregistered thread. Except in rare circumstances, you also want to create thread roots with your threads as well, so that they can freely reference the managed objects.
You can separate your objects into pools for many reasons, e.g. different management policy, different parameters, easy mass destruction, etc. The MPS needs to consider all references in the whole world, but it does not scan them every time. It uses remembered sets to optimise the scanning. A lot.
All thread roots are scanned, but basically yes.
We don't suspend unregistered threads because we don't know about them. But I think you mean thread roots here. |
You have found the relevant code :) |
also
|
generally this would be the case however if we can make certain assumptions about our managed objects, and assert that they hold true, then we can optimize for only select range of threads to be suspended eg, threads that we can gaurentee |
alright
so all registered threads must be suspended then (as previously stated, MPS cannot suspend a thread that it has not been registered with) |
hmmm i implemented a small scanner but my object does not seem to be collecting when it should static mps_res_t managed_obj_scan(mps_ss_t ss, mps_addr_t base, mps_addr_t limit)
{
mps_res_t res;
MPS_SCAN_BEGIN(ss) {
while (base < limit) {
managed_obj_t obj = (managed_obj_t)base;
switch (MANAGED_OBJECT_TYPE(obj)) {
case MANAGED_OBJECT_TYPE_SCANNED_POINTER: {
MPS_FIX_CALL(ss, res = obj->scanned_pointer.scanner(ss, obj->scanned_pointer.pointer));
if (res != MPS_RES_OK)
return res;
base = (char *)base + MANAGED_OBJECT_ALIGN_OBJ(sizeof(managed_obj_scanned_pointer_s));
break;
} #define MANAGED_OBJECT_FIX(ref) \
do { \
mps_addr_t _addr = (ref); /* copy to local to avoid type pun */ \
mps_res_t res = MPS_FIX12(ss, &_addr); \
if (res != MPS_RES_OK) return res; \
(ref) = _addr; \
} while(0) TEST(MPS_finalize, 2_null) {
ManagedObjState state;
managed_obj_init(&state, MANAGED_OBJECT_DEFAULT_ARENA_SIZE);
volatile managed_obj_t p1 = managed_obj_make_with_finalizer(&state, new B_A(), +[](void*p){ delete static_cast<B_A*>(p); });
volatile managed_obj_t p2 = managed_obj_make_scanned_with_finalizer(
&state, new B(),
// scanner
+[](mps_ss_t ss, void*p) -> mps_res_t {
MPS_SCAN_BEGIN(ss) {
printf("b is %p, a is %p, fix b->a\n", p, ((B*)p)->a);
MANAGED_OBJECT_FIX(((B*)p)->a);
} MPS_SCAN_END(ss);
return MPS_RES_OK;
},
// finalizer, set b->a to zero, b->a is currently a GC allocated object, so we must erase it
+[](void*p){ printf("b is %p, a is %p, delete b\n", p, ((B*)p)->a); B * b = (B*)p; b->a = nullptr; delete b; printf("set b->a to nullptr, b is %p\n", b); }
);
// if we set 'a' to 'nullptr' then
// 'B_A' (p1) gets finalized in the first collect
// 'B' (p2) gets finalized in the second collect
//
// if we set 'a' to 'p1' then
// 'B_A' (p1) gets finalized after all roots are destroyed
// 'B' (p2) gets finalized in the second collect
//
MANAGED_OBJECT_SCANNED_CAST(B, p2)->a = p1;
p1 = nullptr;
p2 = nullptr;
printf("first collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("second collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("third collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
managed_obj_deinit(&state);
} [ RUN ] MPS_finalize.2_null
thread stack bottom: 0x7ffd2f937000
reserved and comitted object 0x7f0fac500000 with pointer 0x232c8b0
reserved and comitted object 0x7f0fac500020 with pointer 0x232c8d0
first collect, p may be collected
b is 0x232c8d0, a is 0x7f0fac500000, fix b->a
Total Collection duration: 0 seconds, 0 milliseconds, 365 microseconds
second collect, p may be collected
forward 0x7f0fac500020 to 0x7f0fac580000
b is 0x232c8d0, a is 0x7f0fac500000, fix b->a
pad 0x7f0fac500020
object 0x7f0fac580000 with pointer 0x232c8d0 is being finalized.
b is 0x232c8d0, a is 0x7f0fac500000, delete b
~B() (child is B_A)
set b->a to nullptr, b is 0x232c8d0
object 0x7f0fac580000 with pointer 0x232c8d0 has been freed, setting pointer to zero.
Total Collection duration: 0 seconds, 0 milliseconds, 472 microseconds
third collect, p may be collected
pad 0x7f0fac580020
pad 0x7f0fac500020
Total Collection duration: 0 seconds, 0 milliseconds, 482 microseconds
deinit - destroy all roots
deinit - destroyed all roots
deinit - collect
forward 0x7f0fac500000 to 0x7f0fac580000
pad 0x7f0fac500000
object 0x7f0fac580000 with pointer 0x232c8b0 is being finalized.
~B_A() (parent is A)
object 0x7f0fac580000 with pointer 0x232c8b0 has been freed, setting pointer to zero.
Total Collection duration: 0 seconds, 0 milliseconds, 688 microseconds
deinit - collected
deinit - shutdown
pad 0x7f0fac500040
pad 0x7f0fac580020
[ OK ] MPS_finalize.2_null (7 ms) |
if i set case MANAGED_OBJECT_TYPE_SCANNED_POINTER: {
MANAGED_OBJECT_FIX(obj->scanned_pointer.pointer); volatile managed_obj_t p1 = managed_obj_make_with_finalizer(&state, new B_A(), +[](void*p){ delete static_cast<B_A*>(p); });
volatile managed_obj_t p2 = managed_obj_make_scanned_with_finalizer(
&state, p1, // B_A (p1) is finalized after all roots have been destroyed
// new B(),
// scanner
+[](mps_ss_t ss, void*p) -> mps_res_t {
// MPS_SCAN_BEGIN(ss) {
// // printf("b is %p, a is %p, fix b->a\n", p, ((B*)p)->a);
// MANAGED_OBJECT_FIX(p);
// } MPS_SCAN_END(ss);
return MPS_RES_OK;
},
// finalizer
+[](void*p) {}
); |
seems to be related to #259 (comment) |
void level_2(ManagedObjState & state) {
volatile managed_obj_t p1 = managed_obj_make_with_finalizer(&state, new B_A(), +[](void*p){ delete static_cast<B_A*>(p); });
volatile managed_obj_t p2 = managed_obj_make_with_finalizer(&state, p1, +[](void*p) {} );
managed_obj_assert(p2, MANAGED_OBJECT_TYPE_SCANNED_POINTER)->scanned_pointer.pointer = nullptr; // remove p1
p1 = nullptr;
p2 = nullptr;
// p1 can be destroyed now, no references to it should exist
printf("first collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("second collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("third collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
}
void level_1(ManagedObjState & state) {
level_2(state);
printf("forth collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("fifth collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("sixth collect, p may be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
}
TEST(MPS_finalize, 2_null) {
ManagedObjState state;
managed_obj_init(&state, MANAGED_OBJECT_DEFAULT_ARENA_SIZE);
level_1(state);
// p1 should have been destroyed by now
managed_obj_deinit(&state); // p1 is actually destroyed here
} if we do this then our |
like volatile managed_obj_t p2 = managed_obj_make_with_finalizer(&state, p1, +[](void*p) {} );
managed_obj_assert(p2, MANAGED_OBJECT_TYPE_SCANNED_POINTER)->scanned_pointer.pointer = nullptr; // remove p1 should be the same as volatile managed_obj_t p2 = managed_obj_make_with_finalizer(&state, nullptr, +[](void*p) {} ); but for some reason it isnt managed_obj_t managed_obj_make_with_finalizer(ManagedObjState * state, void * pointer, managed_obj_finalization_callback_t finalization_callback)
{
return managed_obj_make_scanned_with_finalizer(state, pointer, managed_obj_no_scan_callback, finalization_callback);
}
managed_obj_t managed_obj_make_scanned_with_finalizer(ManagedObjState * state, void * pointer, managed_obj_scanned_pointer_scan_fn_t scanner, managed_obj_finalization_callback_t finalization_callback)
{
managed_obj_t obj;
size_t size = MANAGED_OBJECT_ALIGN_OBJ(sizeof(managed_obj_scanned_pointer_s));
while(1) {
mps_res_t res = mps_reserve((mps_addr_t*)&obj, state->ap, size);
if (res != MPS_RES_OK) managed_obj_error("out of memory in make_pointer");
obj->scanned_pointer.type = MANAGED_OBJECT_TYPE_SCANNED_POINTER;
obj->scanned_pointer.pointer = pointer;
obj->scanned_pointer.scanner = scanner;
obj->scanned_pointer.finalization_callback = finalization_callback;
if (mps_commit(state->ap, obj, size)) {
break;
}
}
printf("reserved and comitted object %p with pointer %p\n", obj, obj->scanned_pointer.pointer);
state->total += sizeof(managed_obj_scanned_pointer_s);
mps_finalize(state->arena, (mps_addr_t*)&obj);
return obj;
} even tho in both |
using stack frames seems to work, as even if the compiler attaches the variable to the frame/register they will be gone when the frame exits
|
also using custom memory works TEST(MPS_finalize, 2_null_user_mem_6) {
ManagedObjState state;
managed_obj_t memory[mem_count];
managed_obj_init_with_user_memory(&state, MANAGED_OBJECT_DEFAULT_ARENA_SIZE, memory, sizeof(void*)*mem_count);
memory[0] = managed_obj_make_with_finalizer(&state, new B_A(), +[](void*p){ delete static_cast<B_A*>(p); });
memory[1] = managed_obj_make_scanned_with_finalizer(
&state, new B(),
// scanner
//
// if we do not scan 'a' then 'memory[0]' will be collected
// when 'memory[0]' is set to 0x0
//
+[](mps_ss_t ss, void*p) -> mps_res_t {
MPS_SCAN_BEGIN(ss) {
MANAGED_OBJECT_FIX(static_cast<B*>(p)->a);
} MPS_SCAN_END(ss);
return MPS_RES_OK;
},
// finalizer
+[](void*p){ MANAGED_OBJECT_ZERO_AND_DELETE(B, p); }
);
MANAGED_OBJECT_SCANNED_CAST(B, memory[1])->a = memory[0];
printf("first collect, p should not be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("second collect, p should not be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("third collect, p should not be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
memory[0] = nullptr;
printf("forth collect, p should be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("fifth collect, p should be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("sixth collect, p should be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
memory[1] = nullptr;
printf("seventh collect, p should be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("eigth collect, p should be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
printf("ninth collect, p should be collected\n");
managed_obj_collect(&state); // should collect the original pointer assigned to p
// p1 should have been destroyed by now
managed_obj_deinit(&state); // p1 is actually destroyed here
} ignore the 'p should be collected' stuff, im not sure how to verify collection when using stack memory instead of user memory |
imma close this for now |
jump to #259 (comment)
i am unsure how to go about keeping roots alive as they get eligible for garbage collection immediately after creation
(i assume there would be more that needs to be done in my implementation (below) to correctly support the above tho i am unsure as to what would be needed)
the complete .h and .c file is https://gist.github.com/mgood7123/8a25971c90faa61397701361a0dd8be5
The text was updated successfully, but these errors were encountered: