Replies: 17 comments 30 replies
-
I am finishing up a small data flow framework. The implementation is slightly different from the worklist algorithm, but I would like to share it and hear some thoughts. Some background on the abstractions: API OverviewBlockThis class represents a block. It contains an instruction list and some methods for the easy manipulation, e.g. BlockCFGNodeA class that represents a node in the CFG. It contains a CFGThe class that represents the Control-Flow-Graph. It takes as input a list of WalkthroughThe main idea is that the
|
Beta Was this translation helpful? Give feedback.
-
My code is here. Dataflow Analysis
I copy the testcases under here and generate my own output. The testcases can be easily run following the README using Some Limitations & Rules of my implementationReaching DefinitionI decide not to consider the function arguments here for simplicity and brevity. It is easy to add the func arguments to the Constant PropagationThere is some limitations or so-called rules for my implementation on Constant Propagation analysis. It is also because of simplicity. There's no constant folding and any actual computation involved. For a
It would not recognize that both args of
|
Beta Was this translation helpful? Give feedback.
-
My code is here. CFGI used my CFG implementation from last time, though I modified it to use the function names as the name of the first basic block of a function, or The main testing came from the correctness of the code analyses. I should add a test that shows that the multiple function naming does something reasonable. ReachabilityI first started the project by implementing reachability (tested in cond-args-reach.bril and cond-reach.bril), then refactored it to use a worklist algorithm. The reachability implementation is here, with most of the code starting here. The main issue for me here was in coming up with a way of keeping track of the variable values when they get overwritten. Instead of performing a pass and renaming the variables that get overwritten, I instead just annotated each variable with a list of names of the basic blocks it might have been defined in, and deleted that information whenever it got "killed". This meant that instead of the domain of my worklist algorithm being over sets, it was over a mapping from a variable name to a set of basic block names. While this generally made the worklist functions (merge, transfer, etc.) more tricky than a simple set operation, it seemed easier to me to do that than to rename the variables but also keep track of an equivalence relation over renamed variables, so that for instance I would know that Worklist AlgorithmMy worklist algorithm implementation is here. I started by writing the reachability code in a worklist-ish shape, then refactored the code to use a worklist abstraction once I was reasonably confident that it was working. There was a brief snag in implementing the worklist when I realized that I 1) didn't need to put things in the worklist that were already there (which I don't think causes correctness issues, but did make me sad when looking at the debugging outputs), and 2) that I needed to be popping things off the worklist if I was going to check what was already there or not. The worklist algorithm is really fun! I was super happy with how little additional code getting LivenessFinally, I confirmed my worklist implementation by also implementing live variable checking, implemented here with most code here, and tests in cond-args-live.bril, cond-live.bril, and fact.bril (all based on the example tests). There were two main issues here. The first issue was that I couldn't quite share as much code with reachability as I wanted. Assuming (as we do in this class) that the compiler only needs to handle correct bril programs, anything which is used before its defined is going to be a live variable. However, I wanted to use a simple function to compute the variables which were used. This didn't work, because live cares about if a variable is used before or after it is defined, since defining and then using a variable in a basic block (like The second issue was that I realized that my representation for variables from reachability wasn't fantastic, in that liveness seems to mostly care about what variables exist and are used, while reachability cares about which versions of the variables are used. So I just switched to using sets of variables instead of mappings from variables to sets of basic block names. This code was fun in that it was one of the only times I've worked on an assignment and found that my implementation already handled an issue -- adding args was fine and didn't need any extra work! |
Beta Was this translation helpful? Give feedback.
-
My general dataflow solver implementation is here. DescriptionI implemented five passes/analysis:
These passes can be invoked by passing different arguments to the All passes use the same general worklist solver, each pass has its own merge and transfer functions. Reaching definitions and live variables are intuitive to implement. For CP, CSE, and CF, I added the local LVN in the last assignment to the transfer function. TestsI added all df tests from Bril's examples, and added non-local CP, CSE, CF test cases. All tests can be checked with DiscussionMy idea of implementing constant propagation and other LVN-related passes is to generate value tuples at the input of each basic block. When we perform LVN on a basic block, its upstream value tuples are treated as "pseudo instructions" to provide information about constants and existing expressions. When adding LVN, I noticed one issue: since all the merge/transfer functions are based on set operation so the order is ignored. This could cause some issue for LVN, for example:
When we perform LVN on the My ad-hoc solution for this issue, is to convert the upstream set of "pseudo instructions" from set to list first, and then move all the constant operations upfront. |
Beta Was this translation helpful? Give feedback.
-
LinksPrograms are at: SummaryI implemented dataflow analysis using the worklist algorithm covered in class. I implemented several analyses that seemed interesting to me: reaching definitions, constant propagation, live variables and available expressions. For each of the these analyses, I wrote test cases and used turnt as a regression testing system to make sure changes I made did not break previously checked test outputs. The dataflow analyses can be ran by running TestingI wrote different test programs for each of the analyses. In general, I aimed to see that specific features of each analysis were correct. For instance, for the liveness analysis, I created tests that make sure that variables defined in one basic block would I then used turnt testing at the end to make sure changes I made in the programs did not change the results of the test to be incorrect. ResultsIn terms of results, for this task, reviewed my results from each test and made sure
I checked the output:
which I checked to be make sense relative to the definitions we had be given. DifficultiesWhen I first started, I had trouble understanding the live variables analysis. It took me a bit of time to understand live variables would be variables that were defined, and would have their value used sometime in the future. Another problem I faced was for the available expressions analysis, where I associated each expression with a value, like those calculated in local value numbering. I later realized the available expressions should be thought as combinations of variables that you do not want to recompute. Interestingly, making the worklist solver generic was not too complicated, though that might be because I used python. I can imagine using another language like OCaml would make the worklist solver more difficult to parametrize, especially depending on what containers or functions one chooses to use. |
Beta Was this translation helpful? Give feedback.
-
TeamThe assignment was by our group Ayaka Yorihiro (ay436), Anshuman Mohan (am3327), and Shubham Chaudhary (sc2937). Yay for compilers! Data Flow AnalysisThe python file We implemented the following analyses:
There is a Turnt test specification in Examples
ExperienceWhile Python made using and manipulating Bril JSON very easy, one pitfall we fell into a couple times was with references and mutable methods. And therefore we had to be careful about making sure that if we are returning objects from functions we are returning
When
|
Beta Was this translation helpful? Give feedback.
-
SummaryI implemented constant propagation for my data flow analysis. I used previously-written code to construct a CFG from a Bril program, and wrote some additional code to construct a predecessors CFG (mapping from block name to predecessors). Here is an example run:
Output:
Notice that in the last line of output, the values of a and c are known, but d is unknown. I could further optimize this analysis to do constant folding and assign d the value 37. TestingI used turnt to test, and I wrote several new example programs in the test/df directory. I made sure to test programs with branches that resulted in different variable assignments (and checked that these variable values were unknown after the merge), and some that resulted in the same variable assignments (and checked that these variable values were known after the merge). ThoughtsImplementing constant propagation was pretty straightforward. The only unexpected issue I ran into was with printing my dictionaries directly using print and getting different orderings of variables every time, which turnt did not like. So I had to implement a printer function that sorted by keys. |
Beta Was this translation helpful? Give feedback.
-
I implemented a generic dataflow analysis framework that enables users to easily plug in different algorithms. My code can be found here, and tests are in the Since I have implemented constant propagation with simple control flow last time, and I think it is relatively straightforward to do that within the dataflow framework (which essentially needs to plug LVN into the transfer function), I didn't pay lots of effort on that. Instead, I tried to implement the uninitialized variables and sign analysis, which are not directly covered in the lecture but are widely used in static analysis. Reaching definitionsThis is the textbook algorithm but is still somehow tricky since it actually works on instructions/definitions instead of variables. Therefore, the implementation in class is not completely correct, which also obfuscates with live variables. To get the correct definition references, we need to first label those definitions. I used I also added support for function arguments, which should be added to initial definitions. Live variablesThe provided tranfer function is correct in general but requires more consideration when implementation. Considering the following example, if using the transfer function
To fix this problem, we should do the instruction-wise analysis, meaning Set
By traversing the instructions from back to front, we have In summary, this example shows sometimes the transfer function should be applied per instruction instead of per block. Otherwise, it may lead to a different result. Uninitialized variablesThis algorithm tries to examine whether there exist uninitialized variables in the program. I take all the variables and make them uninitialized at the beginning. When some variables are defined in the block, they are removed from the Set.
Take the above program as an example, this analysis can capture all the uninitialized variables from different paths. Although
Actually, this piece of program is valid in Python, since Python does not have this kind of static analysis to check whether a variable is initialized in all the paths. But for C/C++, this should be an obvious error that will be captured during compile-time. Sign analysisFor sign analysis, it basically determines the sign of each variable. I used the lattice definition, except for For example, for the add operation, the sign analysis should follow the below computation rule.
I only defined the rules for arithmetic operations like
The analysis outputs the following signs. We can see that
Sign analysis actually does a similar thing of finding uninitialized variables. Once the sign of a variable is |
Beta Was this translation helpful? Give feedback.
-
IntroductionI decided to skip writing a single data flow analysis and skipped straight to writing a generic data flow solver. The reason I chose to do this is because I have written live variable analysis, reaching definitions, available expressions, and available copies for CS 4120. ImplementationIn writing a generic solver, I first created a CFG with the ability to get the predecessors and successors for each block in O(1) time. I first created a mapping to help me identify blocks in the CFG uniquely. I did chose to identify each block by the label at the start of the block. I modified Professor Sampson's form_blocks code to generate a label at the start of any block that does not have one. This can occur at the start of a function. From here I iterated through each block, where in each iteration I built up two mappings: one from block name to successor blocks, and another from block name to predecessor blocks. These two maps allow for us to later lookup in constant time the successors and predecessors for any block. The next part was to create the generic Dataflow framework. As discussed in class there are 4 elements to any analysis:
From here I realized that although we were working on sets, there is a distinction between having a set with real and tangible values compared to the top set which is just an idea. I opted to create a representation that captures this: The next step was to build the worklist algorithm. Since in and out, and preds and succs swap depending on the direction of the analysis, I ended up with duplicate code to make this happen. After cleaning it up a little, the work list function fit on a screen as a single page of code. Although I believe I could refactor out some more commonalities, I leave this as future work. I found the actual implementation of this step quite straight forward. Next, I created a few different analysis. I chose to implement reaching definitions, live variable analysis, and defined variable analysis. I chose to do reaching definitions because we discussed it in class. I did live variable and defined variable analysis so I could reuse Professor Sampson's tests, and also because I had both forward and backward analysis to test. The last step was to create some pretty printing functions which was quite trivial. TestingI thought I was smart in implementing analysis that Professor Sampson used so I could compare against his tests. However, I was printing out sets in arbitrary order (since sets in python are unordered). This made it so my output was non-deterministic. I tried running live variable analysis for a small program a few times and eventually I got lucky on the ordering of the prints that But moving to larger programs made the probability of getting the same printout as Professor Sampsons quite low. So I inspected manually for each program in the In testing, I caught some bugs with my live variable analysis. Specifically, I had written the Although my tests have passed the 3 or 4 examples in the test suite, I cannot say that I am very convinced that my code works. In order to be more confident, I would love to test on larger and more complex programs, but I cannot think of another way to do it than manual inspection. I suppose I could hammer out the ordering of printing sets and compare my output to Professor Sampson's output on more programs, but this only shows how correct I am relative to how correct his analysis is. It's better than nothing though. Another way to become more confident is to write transformations that use these analysis. Then I can run the transformed code and compare against expected values for all the benchmark programs. This is what I plan to do in the next lessons which is why I did not spend the time ordering my sets and doing manual inspection on larger programs. I understand this approach is less modular, and in a production environment I would suck it up and make sure the analysis is correct independent to any transformation, but in this learning environment, I hope this is sufficient. |
Beta Was this translation helpful? Give feedback.
-
My implementation is here CFGI went through a couple iterations of what the CFG should be, and settled on what in hindsight was not ideal. I made a class for basic blocks that had a label, a list of instructions, and a variant for the outgoing edge(s): either going to another label, returning something, or a branch. This way I could write my analyses ignoring these control flow instructions. This was very stupid, since branch instructions "use" their condition, so i had to put them back in. In hindsight I would not have done it this way. The actual CFG is three hashmaps: labels -> labels for succs, labels -> labels for preds, and labels -> blocks to keep track of the code. This is partially because it is difficult to hash objects in LISP, so much easier to hash only strings. DataflowI actually believe that implementing the generalized dataflow was easier, as it gave me another layer of abstraction to work with. Also lisp has first class-ish functions, so the syntax was pretty easy, and I could pretty much copy it from the lecture. I made a recursive function to do the flowing, so the nodes are processed as if they are being pushed onto a stack. I'm honestly not quite sure if this is ideal or not, but in my (somewhat limited) testing it didn't seem to be a problem. I implemented everything as a forward analysis, and then sneakily defined helper functions for get-succs and get-preds that have extremely misleading names when you are running a backward analysis :) I didn't swap the in and out maps, so the user has to make sure to put them the right way around when they get them returned (this is the return value of my worklist algorithm) Reaching DefinitionsAfter implementing the worklist algorithm, I implemented reaching definitions partially as a way to test it, and partially because, well, that was the assignment. Again, this was pretty much just copied from the lecture, but I did have to add numbers to instructions. Each instruction in the function (including labels since why bother filtering them out) is assigned an index so that if there are two "identical" instructions they can be easily differentiated. I initially forgot to do this, but it was pretty easy to add in. This was all relatively straightforward, as I used lists for my sets, and lisp has build in union/intersection/set difference with lists. The output for this was not the most pretty, i put a label for each function, and under that a label for each label, corresponding to a basic block. I then gave the list of definitions at the beginning and end of that block. I used the default printing of my internal representation, so it's readable, but not very bril-like Live variablesIn order to make sure I got the backward analysis working properly, I also implemented live variable analysis. This again was pretty much just from the lecture, and didn't present any serious issues. This was when I realized that I needed to include branch instructions to count their uses. Similar to reaching defs, I printed the live variables at the beginning and end of each basic block. I don't know why I was surprised to see that a variable defined and only used in a single block is never reported as live, but this does make sense lol. TestingTesting was one of the most challenging parts of this assignment, as I couldn't just run the code and make sure it did the right thing. Initially, I wrote the CFG, and wrote a function to recover the instructions from it. Then, I verified that program -> cfg -> program preserved behavior on the benchmarks by using turnt. For my analyses, I did most of my work in the REPL, as it was a more integrated experience and helpful for testing bits and pieces of things. For the "real" testing, I again used turnt with the -pv flags so that I could manually inspect the output. When i was convinced it was correct, i ran turnt --save, so that if i found a bug later I could make sure i didn't break anything fixing it. I took the suggestion of using command line arguments to select the analysis, and that made integration with turnt pretty painless. Difficulties/experience/mistakes.Lowlights:
RemarkI should have started earlier and implemented more different analyses. I think doing one forward and one backward means that I probably eliminated most bugs from the actual dataflow though |
Beta Was this translation helpful? Give feedback.
-
I implemented a general dataflow framework in Rust. On top of this framework I implemented Live variable analysis and defined variables. It can easily be extended to support any of the discussed dataflow analyses. CFGMy CFG representation is a struct with maps for predecessors and successors, represented as Strings, and an ordered map from label strings to blocks. DataflowA dataflow pass is defined in my framework as a struct that implements the Dataflow trait, written below:
Item can be set to change the type of items stored in the dataflow analysis sets. A struct that implements this trait is then passed into the general worklist algorithm which calls the corresponding functions. TestingThe framework is tested using a small set of turnt tests. The output is just a simple formatting of the dataflow analysis output, but this can be easily modified for each specific analysis. DiscussionThe most challenging part of this assignment was figuring out the best way to represent the generic dataflow framework in Rust. Dealing with generics mixed with structs in Rust is still new to me, but I think I'm starting to get the hang of it. |
Beta Was this translation helpful? Give feedback.
-
My implementation: https://github.coecis.cornell.edu/awy32/Brilliant/tree/main/lib/df CFGYes, I was the person called out by Prof.Sampson in lecture the other day for using a full fledged graph library to build the cfg. In fact my initial ideas was even worse, as I tried to put the blocks themselves directly as nodes in the graph. This is very expensive and requires the block type to be hashable, comparable, etc., so I decided to just switch to a graph where the nodes are the block names. This has the additional advantage that it's really easy to build up the edges when converting a function to cfg, for if an edge target node doesn't exist yet, you can just create it without having to worry about the instructions in the block represented by the node. One advantage of using the graph library tho, is that I get a digraph so preds and succs are already defined functions. Furthermore, I decided to use mutable array instead of lists to hold the instructions inside each block. This should make traversal by index and applying optimizations much easier. DataflowI built a generic dataflow framework, which turns out to be quite intuitive. The OCaml module system is very helpful here. I basically defined a module type for posets, requiring the appropriate functions and values like Reaching DefinitionsThis one is a bit tricky because OCaml has finicky physical equality. In order to tell various copies of an instruction apart, I need to either add field to the instruction type(which would require rewriting the parsing from json, etc) or to represent the instructions in the poset values in a different way. I chose the latter after Sampson's suggestion on Zulip and recorded the TestingThis has not happened despite my valiant effort. However I would argue that since I'm writing the code in OCaml, the fact that the code compiles means it's probably correct... Other Dataflow AnalysesI plan to churn out a few more analyses, which should be really quick. |
Beta Was this translation helpful? Give feedback.
-
My implementation is here CFGFor constructing the CFG, I relied on the method from Lesson 2 with the Also, I guess I wanted to get ahead of any of the same labels being generated, so I used the Python uuid library to generate block names, but this is a bit overkill IMO. Dataflow AnalysisI didn't get a chance to implement constant propagation with LVN, so I tried to do it here with dataflow analysis. Implementing the worklist algorithm was the easiest part, especially after implementing it for just finding definitions. I structured my code as close to the pseudocode as possible, and it ended up working pretty well with Python. Implementing constant folding required some drawing of CFGs and looking at how to solve the equations. If I had more time, I'd try to modularize my worklist function, which I got close to doing but I lack DFA configs for various analysis which I need to figure out. For demonstration my program
gets analyzed to
TestingI moved my testing directory to be a general dump of all the tests I've created so far. This is one category I need to do more with, and organize each test for specific functions rather than running through all of them. Especially with my DFA not producing executable Bril, a lot of testing was just by inspection. |
Beta Was this translation helpful? Give feedback.
-
My implementation is here. I implemented a generic dataflow analysis solver and the reaching definitions dataflow analysis, which I tested using the given tests. Generic Dataflow Analysis SolverMy generic solver, written in python, implements the takes an initial value, meet operator, and transfer function as its input (along with an optional pretty-printer parameter). It keeps track of a mapping from each block to its input and output, and assumes that each block begins with a unique label (which is accomplished using my cfg implementation). The CFG is represented by
LimitationsAdmittedly, I haven't yet tested my generic solver on an algorithm that requires a backward pass, but the backward pass works simply by inverting the in and out objects both before and after the data analysis, effectively reversing the direction of the control flow. DiscussionThe workflow algorithm was surprisingly easy to implement, but I ran into trouble implementing the dataflow analysis for reaching definitions because I did not realize that in python, when a dictionary is copied, the elements are not, so altering a value in one dictionary will also alter the value in a copy of it. |
Beta Was this translation helpful? Give feedback.
-
Here is my implementation. The data flow frameworkThe algorithm used is exactly the same as the pseudo code provided in class. The I have implemented a generic solver so that any only three new functions and one property need to be defined for new analyses, which are:
It's also possible to add some common helper functions that multiple analyses can benefit from in the "Analysis" class. Dataflow PassI have implemented DiscussionI feel like implementing dataflow algorithms is fairly straight forward. The interesting part was to design a generic solver that can maximize reuses for different dataflow analyses. I feel like there are still a lot of work (tools) could be done on the generic framework to support any new dataflow analyses, and meanwhile make it clean and simple. |
Beta Was this translation helpful? Give feedback.
-
I implemented the generic dataflow analysis algorithm here and tested it on the constant propagation use case. Bellow is an example on fact.bril test with constant propagation:
Constant propagation result (first line in each basic block corresponds to the first instruction on the block):
Limitations:My current implementation does not do constant folding, just propagation (see discussion bellow). DiscussionI'm wondering if df analysis can be hooked-up with constant folding (for example) and provide global constant folding. For example, in the above program, would it be possible to compute the whole eight iterations in compile time and fold everything, including the branches. My thoughts is that this is a very advanced feature for compilers, and very rare real compilers implement it. Am I right? For example, such global constant folding is basically the whole constexpr business in C++11 and higher (that allows to fold the whole program to a single value), and, I suppose, it's extremely complex to implement. Here is an awesome example of what one can do with the global constexpr folding in C++: https://bitbucket.org/jjeka/mathcpp/src/master/ - the code reduces complex computations (with brackets) over const numbers expresses as string expressions (like "(67+987^(7-32))(34-123)+17^2+(-1)") into a single value entirely in compile time. A 500-LoC program is getting reduced into a single line one ;) |
Beta Was this translation helpful? Give feedback.
-
Implementation and TestingI implemented constant folding, live variable analysis, and a (probably very broken) interval analysis here. A lot more detail went into the README there so I don't want to really repeat myself, so I'll leave a brief description here. I also made my dataflow analysis general, so as long as your combine operator followed a monotone lattice definition, it would work. For example, I had a whole "evaluate_expr" function that would take the most specific guess it could get, but if it couldn't guess it it would guess '?' (for constant folding. I tested my tests on lvn (to verify constant folding was working), the dataflow section, and some of my own tests that I thought of to test loops. Live variables and constant folding seemed to work fine but interval analysis kept breaking (or did it?) For example, when I had an infinite loop that kept dividing by two, the interval analysis would say [-MAXINT, MAXINT] for the in block and [-MAXINT/2, MAXINT/2] for the outblock. I mean, I guess, but I feel like it should be more precise than just saying literally maxint and maxint/2. Dataflow Analysis TheoryAfter implementing general dataflow analysis, I'm wasn't sure if I understood why the dataflow analysis works. At first, I sort of intuitively understood monotonicity as "any time you join two variables, make sure you can't get anything more specific the inputs" and that seemed to work for constant folding and live variable analysis, but when it got to interval analysis my programs kept breaking (well, not "breaking" per say, but the resulting variables were way too general), and I thought that assignment operations didn't seem so monotone. So I decided to look into this book that was supplied in class to learn more about the theory behind the algorithm. To be honest, I just handwaved everything past exercise 4.29 as "oh of course if you repeatedly apply functions they'll converge" or something. But I read all the way up until exercise 4.28, and I think my understanding of why the algorithm works has increased up to the point where I'm at least convinced it works. Since I thought that assignment operations were weird - sometimes, you can override a "top" variable with a smaller variable, that doesn't seem monotonic at all. But then thinking through exercise 4.26-4.28, what it's really saying is that if the set of functions that act on each variable are individually monotonic, and thus the entire mapping lattice is monotonic. I think what I was thinking of "monotone" as before was actually part 2 of exercise 4.26, where I thought you were changing the function itself, instead of changing the set of functions that act on the entire mapping. If that were the case, because of part 2 of exercise 4.26, of course changing the function itself isn't monotone, but exchanging one monotone function for another is still monotone. If that's the correct interpretation, I think that's pretty cool - instead of needing to think about whatever f_1...f_n is with the dataflow constraints which I didn't really get, all you need to ensure is that every function you apply to a variable is monotone, which is easier and more elegant to think about IMO. So the two types of operations in constant folding are "evaluate_expr" and "assignment". The former is monotone because I specifically programmed it that way; the latter is monotone because it's a constant function. (at least, that's what I think is going on. I could have just misinterpreted the whole thing, which is def possible since I had to ask a friend for help to understand what was going on in exercise 4.26 and took 2 days to understand that part alone lol) (also, did they get notation wrong? Why is f_1(x) a mapping from L^n -> L, but x_1 = [some mapping from A -> L]? I understood the A->L mapping thing but didn't understand what f_1(x) was really doing, I kind of just interpreted it the way I did above, which is another reason why I'm a little hesitant to just conclude my interpretation is correct). It's a really cool algorithm, regardless - it kind of reminds me of bellman ford because of how you can locally update the answer and achieve a global minimum (or a global minimum to the best of our ability), and it also reminds me of segment trees because of using a mathematical property to generalize operations more abstractly (in the case of segment trees, it's using monoids, here it's using lattices and the idea of monotonicity). |
Beta Was this translation helpful? Give feedback.
-
This thread is for summarizing how your implementation of the data flow framework and its client optimizations went!
Beta Was this translation helpful? Give feedback.
All reactions