Replies: 13 comments
-
SummaryCollaborated with @collinzrj . We implemented the dead code elimination optimization on the trace. If a variable is only used in one branch but not the other, when we eliminate the branch, we will be able to eliminate that variable and other variables that are only used by that. We have also written an example bril code that demonstrates the optimization. Hardest PartThe most challenging part of this task is to figure out how just-in-time compiler works. Bril has provided good abstractions for us, the TestingWe create a test case in "test_jit.bril". It has some dead code that could be eliminated in the function myloop if the program executes the most commonly used branch. Here is the pseudocode. We tried different inputs such as 45 and 100 that would cause the guard to be evaluated differently and compared both the results and the dynamic instruction count.
After the Optimization the bril code looks like
so it is clear that our algorithm successfully eliminates the dead code if we branch to the ".small" branch. If we run the program with argument = 45, the instruction count of our program is 686, whereas that of the original bril.ts is 726. However, if we run the program with argument being 100, the instruction count is 1911 vs 1606, since we need to execute extra instructions such as "guard". "speculate", and we also failed the guard so we waste executing instructions that will be rolled back eventually. |
Beta Was this translation helpful? Give feedback.
-
Will and I worked on lesson 12 together. Summary
Implementation details
What was the hardest part of the task? How did you solve this problem?
|
Beta Was this translation helpful? Give feedback.
-
Summarize what you did.
Explain how you know your implementation works—how did you test it? Which test inputs did you use? Do you have any quantitative results to report?
What was the hardest part of the task? How did you solve this problem?
|
Beta Was this translation helpful? Give feedback.
-
Summarize what you didI collaborated with @stephenverderame on this assignment. Our code and examples can be found here. Implementation detailsFor this task, we decided to trace at basic block boundaries. This is simply because we did not want to deal with inserting new labels and jumps for stitching the trace into the program. We also stopped tracing at prints and function calls, because prints induce a side effect and we did not want to inline functions for this assignment. We first run the program to completion in the interpreter and trace the entire output. Our JIT allow the user to specify how many basic blocks they want to trace and will create a trace up to that many basic blocks (stopping of course at prints and calls). Because we start tracing at the beginning of main, we always start the program at the trace. We created a new label that points to the beginning of main, and all conditionals turn to guards with that label so that if we abort we just run the program normally. Because of the way we formulated the trace to always stop at basic block boundaries, the last instruction of the trace will always be a jump to the correct basic block in the original program, allowing us to do no modifications to either the trace or the original programs in order to stitch them together. TestingFor testing, we ran armstrong, collatz, perfect, and loopfact with and without tracing, We also tested each program with different arguments such that we tested runs We summarized the results of the number of dynamic instructions in the following
what was challengingWhen we started, we thought this task would be very difficult. It seemed like there were a lot of uncertainties for how to do things like stitching the trace back into the program, especially in the middle of a basic block or in the middle of a function. In the end, we formulated the tracing to follow basic blocks so that the stitching was extremely straightforward and easy to reason about. We ran into a number of challenges along the way:
|
Beta Was this translation helpful? Give feedback.
-
I worked with @SanjitBasker on this assignment SummaryImplementation DetailsFor this assignment, we implemented a JIT compiler for Bril that injects speculative traces into a Bril program while interpreting. To generate these traces, we modified the TypeScript Bril interpreter to start tracing at the start of the program and periodically stop tracing. We chose to stop the tracing during the following instances: when print was called, when a function was called, when any memory instruction was called (alloc, free, load, store, though in hindsight it might not have been strictly necessary to include load), and when a backedge was reached (we deduced when this occurred by adding indices to each instruction and comparing the indices of consecutively executed instructions in the same function). We also stop tracing when a function is returned from. While we periodically keep track of these traces throughout the entire execution of the program, we only output one of these traces at the end. We chose to output the first nontrivial trace (which we defined as a trace that contains a branch instruction) that we come across, or if none exist, the last trace that we compute. For branch instructions within a trace, we also add an additional field that denotes whether or not the branch was taken. Now, to stitch the trace with the original program, we first manually added the indices of all the instructions to the original program. Then, to find the correct position to insert the trace into, we simply compared the position of the first index in the trace to the relevant index in the program to insert. We start speculating, insert all the instructions from the trace while removing jumps and replacing branches with guards of the appropriate variable (with appropriate boolean variables injected if branch was not taken), add a failure label to abort to if the speculation fails, and add a success label to jump to if the speculation succeeds. If the speculation fails, the program will fall back on its original code. We then remove the indices we added to ensure proper parsing and output the new program. TestingWe tested our JIT compiler on a few different benchmarks with different inputs and measured the dynamic instruction counts of the baseline and traced versions of each benchmark on the given input. The ones we chose were catalan, collatz, and primes-between. Our results are shown below:
From this, we can see that while the catalan and collatz benchmarks have potential to abort tracing frequently, resulting in a worse performance, the primes-between benchmark only performs slightly worse with tracing. In a real-world JIT scenario, the traced code would be run much faster than the rest of the interpreted code, which means that primes-between would likely have a faster runtime with the trace our JIT compiler chose than the baseline. DifficultiesInitially, we tried to construct all possible traces before realizing that this would be too intractable. Besides that and the issue of dealing with trivial traces, though, we found this assignment to be one of the more straightforward ones. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
SummaryDetailsI modified the TestingI tested the transformations on the The following table contains selected performance measurements. Bold indicates the arguments used for tracing.
DifficultiesThe tricky part of this task for me was deciding when to start/stop tracing so that the trace could be easily stitched back into the program. I ended up performing tracing at the granularity of entire functions to make stitching simpler. |
Beta Was this translation helpful? Give feedback.
-
SummaryImplementation Details
TestingDue to the limitations of the optimizer, I only tested my implementation on several hand-picked programs. They can be found under
ChallengesThe biggest challenge to me is to find a correct hot trace from the traces. A loop in the trace could appear like repetitions of "header - body" or "body - header", while only the first is correct. This fact requires me to start scanning from the very beginning to leverage the fact that the loop header dominates other blocks in the loop. After ensureing the hot trace always starts from the header, I can safely insert it before the label of the actual loop header. |
Beta Was this translation helpful? Give feedback.
-
Summary
Details
Testing/EvaluationI first manually verified that the result of stitching the trace is what I expect by verifying the CFG. I also verify that the output of the stitched trace is the same as the output of the original program. Below an example the CFG of a program and the CFG after stitched with its trace. I then used the DifficultiesThe most difficult pat was probably finding the right spots in the original program to insert the trace was a bit tricky. I had to make sure that the dummy labels were inserted at positions where the specuation could break out to and retain the same behavior as the original program. |
Beta Was this translation helpful? Give feedback.
-
@xalbt , he-andy, and I worked together on lesson 12. SummaryImplementation
Difficulties
Testing... |
Beta Was this translation helpful? Give feedback.
-
@rcplane and @zachary-kent worked together SummaryWe successfully captured, optimized, and measured dynamic instruction counts for interpreted bril programs. Implementation
Tests and Results
Difficulties
Generative AI
|
Beta Was this translation helpful? Give feedback.
-
@JohnDRubio, @20ashah and @AliceSzzze worked together on this task. Our implementation is here. ImplementationWe implemented a version of a tracing interpreter which used the method JIT policy of beginning tracing at the beginning of each function. The interpreter traces each function until it reaches an instruction that it needs to bail out of such as: call To bail out, we try to backtrack the trace to the last executed block, commit, then try to jump to the block we just bailed out of. This helps us trace the instructions executed up to the bailed out block, instead of not tracing at all whenever we encounter a problematic instruction. For example, we just executed block A in our trace, and we are now executing block B, where we see a print instruction. We preserve all the traced instructions up to the end of block A, commit, and then add a jump instruction to the original block B. When we see a We do not modify functions with no ResultsWe tested the tracing JIT on all of the benchmarks in benchmarks/core directory using brench. We list the dynamic instruction count changes for baseline, tracing, and tracing + LVN + trivial DCE below. Unsurprisingly, tracing performed poorly on recursion-heavy benchmarks, as we did not inline functions and the path each of the recursive calls probably differs just enough to make the tracing inapplicable. The benchmarks that benefit from tracing usually benefit from the lack of Summary statistics
plot of % dynamic instruction count changebreakdown of % dynamic instruction count change by benchmark
Testing on different inputs (using the given args to generate traced code)A benchmark that tracing did well on: A benchmark that tracing did badly on: ChallengesThis was a fun task but there were definitely a few tricky parts. One challenge was correctly bailing out of traced code when a guard failed. Another tricky part was considering how to handle interprocedural tracing. We didn’t end up implementing interprocedural traces but after some discussion, we believe that interprocedural traces that would be worth using would be traces with calls to functions that do not contain branches or recursion. We believe this would prevent any “overfitting” - meaning cases where the traced function execution is too specific and is not likely to occur exactly the same way again. Moreover, there does not seem to be a good way to bail out of when you are a few recursive calls deep without copying the code from the inlined function altogether. *updated after bug fix |
Beta Was this translation helpful? Give feedback.
-
I worked on this task with @emwangs SumaryHere is the link to the repository: brili-tracing Implementation
Testing and Evaluation
|
Beta Was this translation helpful? Give feedback.
-
Here's the thread for the dynamic compilers task, which involves doing some speculative transformations on Bril IR!
Beta Was this translation helpful? Give feedback.
All reactions