Replies: 16 comments 33 replies
-
IntroductionI implemented a reference counting garbage collector in the ImplementationThe CollectorI created a class
I kept track of counts to objects using a map with key of
Using the CollectorThere are a few places where the collector must be put to use. The collector must be used for the following instructions:
For an For a Although not entirely clear that this was allowed by reading the docs, the interpreter allows us to call Lastly we must handle the case where a function calls In addition to this, we use the The Free instructionNow there is no need for the The reason I decided to make it a Pointers to PointersI did not implement GC in the case where there are pointers to pointers since I do not recursively deal with children. I did not check and see if it was even possible to create a pointer to pointers in bril. TestingThe Free InstructionAs explained above, free acts as a TurntI used I am passing for all benchmarks. Test CasesI also added support for 6 test cases: no-free.bril
This test case makes sure that memory is automatically freed. Otherwise, the interpreter would throw an exception. loop.bril
This test case makes sure that gc-call.bri
This test case checks to make sure that we increment and decrement all variables at the start and end of a function call. double-alloc.bril
This case checks to make sure we handle the case where we id-ptr.brill
This test case makes sure that we are doing a ret-alloc
This test case makes sure that we do a SummaryOverall, I'm pretty confident with my code since I have identified many specific cases where the collector must be used and why it needs to be used. Also because it passes for all the benchmarks. I also enjoyed this project. I felt that my solution was clean and modular. This project made me think thoroughly how to use the collector. In class we discussed how to implement the collector but didn't talk that much about how to use it. I had to experiment, test, and iterate to a working solution. This was my favorite task so far. |
Beta Was this translation helpful? Give feedback.
-
@5hubh4m, @ayakayorihiro, and @anshumanmohan worked together on this assignment, and our work is here. ImplementationWe implemented a straightforward stop-the-world tracing collector with one space and no compaction. We found this assignment relatively manageable. Our work is in two broad parts: Tweaks to MemoryWe need a few edits to the memory model to make it amenable to garbage collection. The main change is in The Actual CollectionA We change the existing The method TestingWe tested our work on the bril examples in:
Of course, only a small number of the benchmarks use the memory extension. We found that outputs were unchanged, which is good, and that no memory leaks were introduced, which is better. ChallengesWe thought this went pretty quickly. The main challenge was getting used to some of the funkier syntax and language decisions that the TypeScript language makes. |
Beta Was this translation helpful? Give feedback.
-
I implemented a reference count garbage collector. My implementation can be found here. DetailsI implemented a new class called GC. The main structure that I am using to keep track of the reference counts is the following map: private readonly referenceCounts: Map<Key, number> To make my implementation minimally invasive, I created the following handleAssignment(dst: string, state: State, pointer: Pointer) I've added this call only for the clean(state: State) Backwards CompatibilityKeep
|
Beta Was this translation helpful? Give feedback.
-
SummaryCode is at: code I implemented ismple reference counting for the Bril language, including the memory extension. To run, you can do In this assignment, because I focused on reference counting, I only needed to look at certain instructions. I considered id, alloc, free, store, load and ptradd instructions. To allow for the counting analysis, I created a map from pointers to the count for that pointer, and also created a map allowing pointers to point to another pointers. Free is trivial, and eliminated. Alloc adds a new pointer, and a counter is incremented to be 1. If there was a prior pointer to the variable that was given an allocation, that prior pointer is decremented. Anytime a decrement occurs, it is done recursively, such that if an object's counter reaches 0, if the object itself points to another object, that other object also has a recursive decrement operation applied. Stores cannot change the counters for any pointer. However, a store may allow a pointer to reference another pointer. The map of pointers pointing to other pointers is updated here. Loads can allow another variable to reference a pointer, if a pointer value ends up being loaded. This means loads increment a counter for a value if the value is a pointer. Finally, if ptradd ends up assigning a different variable to a pointer, then the pointer gets its counter incremented appropriately. TestingTo test, I used brench to compare just the brili interpreter, versus the brili-gc interpreter, in which the free operation has no effect. I tested over every benchmark in the bril repository, and made sure the outputs were identical in both and there were no memory freeing issues. DifficultiesLearning Typescript was a little difficult. In particular, I was not aware Typescript did not have keep types at runtime, which made it hard for me to figure out how to discriminate between various union types. I later ran into issues trying to figure out whether a value was a bigint, number or boolean, which caused some debugging trouble as well. A problem more related to reference counting was how I needed to free all references at the end. Because reference counting is conservative, it leaves some references at the end that it cannot eliminate at runtime. I forgot to do this at the end of the program and had to add this in. I also initially failed to recursive update the counts for chains of pointers. I also had an issue where if I have a pointer to an array, I failed to check all the elements of the array, which could be pointers. I fixed this issue, by having the pointer map point to a list of potential pointers, which in the array example would be the instantiated pointers in the array. Perhaps something that would be interesting to study would to figure out better schemes to do reference counting. In particular, it would be interesting to see if there are better places in the program to defer reference counting to. For example, rather than doing reference counting all at once, it would be interesting to do it at certain locations in the program. |
Beta Was this translation helpful? Give feedback.
-
My implementation is here. DescriptionI extended the
ImplementationI added the type State = {
env: Env,
readonly heap: Heap<Value>,
readonly gc: GarbageCollector,
readonly funcs: readonly bril.Function[],
// For profiling: a total count of the number of instructions executed.
icount: bigint,
// For SSA (phi-node) execution: keep track of recently-seen labels.j
curlabel: string | null,
lastlabel: string | null,
// For speculation: the state at the point where speculation began.
specparent: State | null,
} At the end of each function, TestI disabled the $ turnt benchmarks/*.bril All testbench cases passed, which indicates that the garbage collector doesn't alter the result or break the program. |
Beta Was this translation helpful? Give feedback.
-
My implementation is here DescriptionI modified brilirs to support a simple mark and sweep garbage collector. The main design decision I had to make was when to run the garbage collector, which for simplicity i run before every allocation. In reality, this could be reduced to every nth collection, but for the sake of a toy implementation it was convenient to see it happening every time (more abt this in testing) Implementationi had to extend the Environment type to include all of the variables in scope in parent functions, as they also need to be used as roots for the trace. Then, i made a free_garbage function which frees either the reachable or unreachable things, depending on a flag. It uses the typical white/grey/black graph traversal to determine reachability. The reason for the functionality of freeing all reachable things was that I call this at the end of main so that the check for unfreed memory is still helpful TestingTo test this, first we need to determine what we want it to do. We want to remove all the frees, and have the same behavior, as well as not having significantly increased memory overhead. (It would be possible to just free everything at the end of main on these benchmarks probably...). Part 1 of testing was using turnt on the benchmarks: I removed all of the frees, and ensured that they had the same output. They didn't, since dyn instr count was different, so i turned off that check, and this worked without too much issue Part 2 was to make sure that things were actually being freed when they should. For this, I added prints to the mark sweep process, and then wrote some tests that alloc in a loop, and then inspected the prints. The frees were happening when i wanted them to, and were happening recursively, so all good. DifficultiesThe above discussion may trick you into thinking this was easy, but I assure you it was not. I had never even seen rust code till a few days ago, and the learning curve was quite steep. I ended up passing the old environments around via deep copy, as I could not figure out how to use a stack like data structure to share them. In the future, I will attempt to learn languages by reading tutorials, and not by working on preexisting projects. |
Beta Was this translation helpful? Give feedback.
-
Since I am an aficionado of Python, I did not use the TypeScript-based interpreter for this task. Instead, I built a Python-based Bril interpreter Bril-py InterpreterUsing The virtual machine takes the
I used the tests in previous assignments to test the correctness of my interpreter, and they all perform well and output the same results as the original Bril interpreter. Garbage CollectorAfter I built the bril-py interpreter, the garbage collector can be easily plugged into it. I implemented a reference counting garbage collector. Only several instructions may cause the references to change:
I removed the In the beginning, I wanted to do something fancier, but I found Bril did not have those advanced language features, so even having a strong garbage collector, there are no test programs in Bril that can leverage these features. For example, I had a hard time thinking about how to construct a reference cycle in Bril, but later I found it seems it is impossible since Bril does not have OOP facilities and does not allow pointers pointing to pointers. For a similar reason, tracing garbage collectors may not be that useful. Common Bril program can be executed in less than one hundred lines, and function calls are also very limited, which means most of the garbage collection can be done when the function returns. |
Beta Was this translation helpful? Give feedback.
-
My code is here. ImplementationI modified the typescript implementation of brili here. My first thought was that the easiest way to do this would be to just modify the Heap object directly, but then I realized that that's kind of at odds with my interpretation of the assignment -- I'm not imagining that I'm rewriting the whole memory allocation/freeing process, I'm imagining that I'm changing the language that's built on top of that. So with that being said, I created a refcounter object to pass around as part of the state. The main complicated parts of the refcounter were 1) keeping track of references which stopped existing when we returned from a function, and 2) a maybe-bad choice that I made in how to deal with the fact that after freeing a pointer you still had access to its variable and could technically do stuff to the variable as long as you didn't try to read from it, but that accommodating this shouldn't break the reference counter. Simpler parts of reference countingI incremented the reference count whenever the program IdOne way for multiple references to exist to an object is to use the id call. So I have it check if it's It's a little more complicated than that, since if you overwrite a pointer variable, then that pointer loses a reference, and you have to decrement it. This is tested in id_switch.bril. I realized during the writeup that it's also a little trickier than that, since there's also a case (tested in multi_id.bril) that if you
Pointer additionPointer addition is also a little tricky, with basically the same pitfalls as id. Keeping track of references when entering/exiting a functionExiting a functionI quickly realized that one of the main points that we have to decrement a refcount is when we leave the function where that variable was defined. So I wrote a function to decrement the refcounter for every variable that we had in the environment if it was a pointer. But then, I realized that you shouldn't decrement the return variable, so I fixed that. Entering a functionThis worked reasonably well, but when I went to test it it was wildly wrong. The main issue was that variables could be passed to a function as arguments, and so if you pass a variable as an argument to the function and then return from the function this should be a noop in terms of the refcounter. To fix this, I incremented the reference count for every argument to a function when the function is called. I don't 100% feel that that was the right choice -- it seems like conceptually it would be better to clean up every variable that wasn't an argument, but since I'm writing in typescript I didn't quite know now I wanted to do that and did this instead. Manipulating freed variablesThe weirdest thing about my implementation came up around freed variables. The basic problem is that when you free a variable the refcount should go to zero, but the variable still exists!
This is totally benign when you allocate then free a variable in a function and then return from that function, but as I understand it bril is okay with you using the variable for a freed pointer as long as you don't try to read from the variable. My ideal logic would just be that freeing a variable also removes it from the environment, but that would crash the bril interpreter when you try to use it later with an error related to trying to use an out-of-scope function. So instead, I mark it as a dead reference, and then if you try to use it again I provide a more helpful error message about trying to use a freed variable. Basically this entire deadref idea is more precise bookkeeping around the fact that a pointer was freed but its variable is still in scope. Maybe setting it to undefined would be a better way of dealing with this. TestingHelpfully, the bril interpreter reference implementation throws an error message if you make it to the end of a program execution and there's still anything in your heap. This means that simply not-crashing means that your garbage collection worked well. I tested by finding all of the benchmark code which uses alloc, commenting out all of their free statements, then checking that I also wrote some special test cases in |
Beta Was this translation helpful? Give feedback.
-
My implementation is here DesignI implement a Reference Counting-based Garbage Collector. It is implemented in I implemented several functions for our
I need to update this map using the above functions when encountering every instruction:
The reason we Build the new brili interpreter with GCunder the GC branch in my
TestI created several testcases with different features here. I also removed all the
Discussion
In It's possible to support above example, but if |
Beta Was this translation helpful? Give feedback.
-
My implementation is here. I implemented a reference count based GC for bril-ts. Here is how it works. ImplementationI defined a class GC that incapsulates my GC and pass it in the
Then I used the GC class to handle the following scenarios:
TestingI used my own set of test-cases here to verify that it works. It passes all my cases. However, some programs from the Limitations/IssuesLoops (SOLVED, but it raises the recursion issue explained bellow)With my approach, I got an issue that I don't know how to solve in a more-or-less easy way. The problem is that my current implementation does not know how to deal with loops. Every time it executes assignment in a loop, it keeps incrementing the reference counter for the corresponding memory location. And since there is only one pointer that corresponds to this memory location, my I solved this problem by tracking the exact Non-SSA allocations (No idea how to solve)My GC can not handle non-SSA codes within a function. Basically, if I have smth like this:
It does not work, as technically, each Recursion (Can be solved with some extra efforts)My GC does not handle recursion well either. Basically, consider the following:
Every iteration of the recursion, we add a new reference Sidenote: A "Workaround" to release all memories even with the aforementioned issuesOne workaround that I tried was just releasing all not-released memories (memories that still have non-zero reference counters) after the |
Beta Was this translation helpful? Give feedback.
-
I implemented a semi-space tracing garbage collector in brilirs. My implementation is available here. ImplementationThe majority of my implementation is based on extending the Heap struct to support garbage collection. A semi-space collector is certainly not the most space-efficient garbage collector for an interpreter like this, but I implemented it in a way that would be more amenable to a compiled language. A large array is pre-allocated to represent the heap and it is logically divided into a top and bottom half. The actually garbage collection is based on Cheney's Scanning Algorithm and allocation is performed with bump pointer allocation. As this is a semi-space collector, every time garbage collection runs all live objects are copied to the other semi-space and therefore compacted. On every allocation the garbage collector checks whether garbage collection should run, stopping the world to run collection if it decides it should. Garbage collection is run when the amount already allocated plus the amount to be allocated is greater than some threshold. This threshold starts out as a small power of 2 and is doubled every time garbage collection is run. This limits the number of live objects for programs will small amounts of memory allocated at a time and prevents programs that allocate large amounts of memory from running garbage collection on every allocation. I got this idea from crafting interpreters. TestingI tested my implementation on the bril benchmarks that allocated to memory. I deleted all free instructions from each of these benchmarks and then compared the output before and after switching to garbage collection. All benchmarks pass this test, and I added print statements to manually check that garbage collection is actually running (we could trivially get the benchmarks to pass by just leaking memory). ChallengesThis assignment took quite a bit longer than I expected. The most challenging part of this assignment actually had very little to do with garbage collection itself, but with getting brilirs to keep track of the entire stack at any point in the program. The previous implementation only keeps track of variables in the current function scope, but we obviously need to have access to the entire stack to determine which objects are live. We also need to be able to be able to modify the roots on the stack as we are moving some objects. I found this pattern a bit challenging to retrofit into brilirs, especially when having to deal with the borrow checker. I settled on extending the Environment struct to contain all scopes up to that point in the program (essentially renamed the previous Environment to Scope and then kept a list of Scopes in the environment). The implementation of this Environment is below:
|
Beta Was this translation helpful? Give feedback.
-
I implemented a reference counting garbage collector for the Rust interpreter. Sorry for the late post. ImplementationMost of the extra information is kept track of in this struct in
Where I also have the following member functions attached to
The algorithm is roughly this:
Note that the returned pointer value from a function call is handled normally just like any other assignment. JourneyI was ambivalent between reference counting and tracing, but decided that learning Rust was hard enough that reference counting was more doable. TestingFirst I yeeted the content of the ChallengesRust. |
Beta Was this translation helpful? Give feedback.
-
My implementation of the brili interpreter with Reference Counting Garbage Collection here. RCGCThe reference count garbage collector (
As with reference counting, once a reference is decremented to 0, the object is first checked if it points to a reference, which is then decremented. I want to test this more, but as of now it passes the simple test case of TestingFor testing, I took the benchmarks which explicitly have memory operations, and either removed the free instructions (in One thing I missed when initially implementing my garbage collector was removing references at the end of main. It seems like most of the benchmarks declare references in main, and the only time they can be removed is at the end of main. This led to some initial confusion, but it was pretty easy to debug. |
Beta Was this translation helpful? Give feedback.
-
I modified the bril interpreter to do reference-counting garbage collection. My implementation is here ImplementationI created a new class called GarbageCollector has the following fields
and methods
Before evaluating a function, we call There are a few cases that need to be modified for evaluating instructions:
Also note that for the TestingSince |
Beta Was this translation helpful? Give feedback.
-
I implemented a simple reference-counting garbage collector here. ##Implementation GC maintains two fields which are:
There are basically just two methods, and associated helper functions to increment/decrement all refcounts.
The above part is not very interesting, and I am assuming that all reference counting GC would have to maintain the same piece of information. The actual tricky part for this assignment is to figure out when and how would we like to use those methods. Here are some scenerios I have thought about that actually involves any GC tracking:
TestingI wrote 6 simple tests to cover a range of scenerios, including (recursive) function calls, |
Beta Was this translation helpful? Give feedback.
-
For this lesson, I (attempted to) implement a reference-counting garbage collector. My code can be found here. ImplementatonI introduced a new class,
This increments/decrements counts in a few circumstances:
I turned IssuesThis garbage collector currently does not consider recursive pointers (pointers to pointers). More importantly, while my implementation passes a few simple test cases that I wrote, it fails miserably on the benchmarks. I found debugging the garbage collector to be quite difficult since it is hard to pinpoint where the excess reference counts are coming from and where they should be getting decremented, and unfortunately I am out of time to debug this. |
Beta Was this translation helpful? Give feedback.
-
These tasks are about implementing a garbage collector!
Beta Was this translation helpful? Give feedback.
All reactions