 Hi and welcome back to program analysis. This is video number two in the lecture on path profiling and what we'll do in this second video is to look at the ball-laros algorithm, which is an algorithm to address the path profiling problem, which I've introduced in the first video. The way we look at this algorithm here in this video is by looking at it for DAG, so for directed acyclic graphs, which means we do not look at arbitrary control flow graphs yet. I'll leave this for the third video, but at a slightly simpler problem, simply because it's easier to explain the algorithm like this, but we'll see in the third video how to actually generalize it to arbitrary graphs. So let me start by giving you a one-slide summary of this ball-laros algorithm. By the way, the algorithm is called like this because it has been invented by Tom Ball and Jim Larros, two computer scientists who are interested in compilers and related topics. So in a nutshell, the algorithm works as follows. So it starts by assigning a number to each path through our graph. So basically each of the path that you can take through a control flow graph has one number. Then it computes this number while going through the graph by incrementing a counter at every branching point. So essentially we start at the entry node and then at branching points increment the counter such that at the end we can tell from the sum of these increments which of the paths we have actually taken. And the way this is done has two very nice properties. One is that this path encoding is precise, meaning that there's a single unique encoding for each path through our graph. And it's also what can be computed in a way that is minimal, which means that the algorithm instruments the minimal subset of edges or more precisely a subset of edges that imposes minimal cost. So we get a precise path at the end, but have some proof that this is actually imposing the least possible overhead. So let me illustrate this idea using the running example that we've already introduced in the first video of this lecture. So here's our control flow graph again. And now what the algorithm is doing is to basically add some instrumentation to the normal execution of this program. Specifically, there are two kinds of instrumentation that are added. So one of them is that we're adding some state or more precisely a counter. That is the number that we are summing up in order to get the path encoding. And let's just call this R. And then we also have an array of all the counts that we are producing, where basically every path through the program has one entry in this array. And the value at this entry corresponds to how often we've seen this path being executed. And we call this array counts. And now given these two pieces of instrumentation, I'll just show you what the algorithm will produce on this example control flow graph here. And then later we will see how to actually get to these numbers that I'm adding here. So for this example, what the algorithm would do is to initialize our counter R to zero, if this edge is taken, the one from A to C. It would initialize the counter to two if we take the edge from B to C. Or if you take the edge from B to D, then it will initialize R to four. And then on the edge from D to E, it would take whatever R currently is and just increment this value by adding one to it. And then when we reach the end of the program, our exit node F, we access our count array at index R and then just increment whatever value is there to indicate that the path represented by the value R has been executed another time. So now given this instrumentation, let's have a look at our six paths again. So all the different ways one can go from A to F. And let's have a look what encoding they would get. So which number is basically computed by adding or initializing R. So let's start with the first path here, which is this ACDF path. So if we started A and then go to C, we will set R to zero. Next, we go from C to D and then from D to F, which is also already the end of our program or our code that is represented here. And because R is still zero, that means that the encoding of R will be number zero. Now let's look at the second path, ACDF. So for this one, we also initialize R to zero here, then go from C to D, from D to E, where we add one onto our existing value of R. So R will now be one. And then from E, we go to F. So at the end, the encoding of this path is one. In a similar way, let's have a look at the third one, ABCDF. So ABC makes us take this edge. So we initialize R to two, then go to D and then to F. So we do not change R anymore, and therefore the encoding is two. For the next one, ABCDF, we will initialize R to two and then also take this edge here, which means that we at the end will have the encoding three. And now let's also have a look at the two remaining ones, ABDF. So for ABDF, we will path through this edge and then not change R anymore. So this path is encoded with this value four. And the final one will go through this edge and also through that edge, which means that the encoding at the end is five. So what you can see is that every path gets one and only one unique encoding. And we get this by only executing these few statements that we've added to the original programs, which sometimes initialize a value or increment a value. And at the end of the code that we are profiling just takes this added up value and uses it as an encoding of the path that has been executed. So now we may ask, well, wonderful, how do we get these blue instrumentation statements because they seem to exactly do what we want. And this is what I'm going to tell you next. And before doing that, let me just state a few assumptions that I'm making here while explaining the algorithm. So first of all, the algorithm is not explained for arbitrary control flow graphs, but only for DAX, so directed acyclic graphs. And we'll see later on how to lift these restrictions. The second assumption is that we have n path through this graph and they are simply numbered from zero to n minus one, just as we've seen it in the example. Another assumption is that our graph has a unique entry and exit node, which is a simple to fulfill assumption if the graph doesn't look like this anyway, because we can always add another entry node or another exit node if needed. And finally, we assume that there is an artificial back edge from the exit node to the entry node, which actually breaks the first assumption that the graph is acyclic, but for this artificial back edge, this is basically an exception from this assumption to make the overall algorithm work. So let's just illustrate these assumptions using the example graph that we have here. So first of all, this graph is indeed acyclic. And the reason simply is that there's no cycle in here. So there's no loop or anything that brings us back to one of the notes that we have already seen earlier. Then we do have a unique entry node and also a unique exit node. So this one A is the one and only entry node, where we start the execution of the code. And this one, node F, is our unique exit node. And then the last assumption was that we have this artificial back edge from the exit node to the entry node, which I'm adding here. So this one will be this artificial back edge that we'll use in the algorithm. All right. So now having stated these assumptions, let's have a look at the algorithm, which essentially consists of two main steps. So step number one will assign integers to each of the edges that we have in our graph. And the goal of this first step is to assign these integers in such a way that the sum along the particular path yields a unique number for this path. So basically we want to get this number that represents the encoding of the path. And this first step alone is enough to reach the precision goal that the algorithm has. So by just performing this first step, we know for sure that every path will have a unique number. Now the second goal was to also impose minimal effort while computing this encoding of every path and while keeping track of how often that path is executed. And in order to do this, the second step of the algorithm is to assign increment operations to edges in a way that does not just use this integer for every edge, but basically tries to reduce the number of increment operations and initialization operations that we need. So the goal here is to minimize the additions that are actually performed along the edges. And in order to do this, the algorithm will only instrument a subset of all edges that we have in our graph. Now in order to know how to minimize the actual effort, you need to make some assumptions about how often these edges are executed, because ideally you want to add instrumentation to those edges that are not executed very often, because then these increment operations will impose the smallest possible overhead. And to do this, we somehow assume that we know how often each edge is executed. For example, this is a piece of information we could get from the edge profiling approach that we've talked about in the first video of this lecture. So now let's have a look at the first part of this algorithm in some more detail, where we want to compute this integer value for every edge such that the sum of the edge values that are along a path gives us this encoding of the path, so this unique value that identifies the path. In order to do this, what the algorithm does first is to associate value with each node in the graph and we call this value num paths, which stands for the number of paths from this node and to the exit. So in order to compute this num path, the algorithm does the following. So it visits every node in the graph in reverse topological order. Reverse topological order just means that we start at the end and then visit nodes by going backwards such that we never visit a node before having visited all of its successes. And so while going through the graph in this order, we distinguish two cases. If n is leave node, so if it's basically a node that does not have any successes, which means it's one of the, or it's the unique exit node, then we set num paths to one. And otherwise, we set num paths to the sum of the num paths of the destinations of all the outgoing edges of the node. So basically, if I'm at some node and the two outgoing edges, let's say one has num path of three, the other one has num path of four, then when I'm here, I basically have seven ways to go to the exit because I could go this way and get the three from here, or I could go that way and then get four from here. So together, this gives seven. So this is the basic idea of computing these num paths. This value num path is of course only a helper value on the way to computing the integer value for every edge that we actually want to compute. So just as a reminder, this integer value should be such that we can add up the integer values of all edges along a path to get the unique encoding of that path. And here's the algorithm that shows how to compute this integer value and at the same time compute this helper value called num paths. So what this algorithm does is the following, it visits all nodes in reverse topological order and then distinguishes two cases if the node is a leaf node. So basically, if that's our exit node, then we only initialize num path and we initialize it to one because by definition we say there's one path to the exit if we are at the exit. And in the other case, so if n is some other node, we do the following. We first initialize the num path value of this node to zero and then we visit all the edges that go out of this current node n. So all edges from n to some other node m and do two things. First, we set the value associated with this edge to the current value of num paths of n and then we increment num path with the num paths of the target node m. And then we do this for the remaining edges so that at the end every edge outgoing of n has some value associated with it. So let's illustrate this algorithm and this way of computing an edge value for each edge in our graph using our running example again. So what you see here is just the control flow graph that we've seen before and now we want to visit each node in this graph in reverse topological order. So essentially what this means is that we want to make sure that we visit every successor of a node n before visiting n itself. So for our example here that means we're starting at the exit node f and then the question is do we visit d or e at first? If we would visit d next then we would have visited d before we have visited one of its successors because we haven't seen e yet. So that means we need to first go to e and then can go to d. Now going backward from d we can look at b and c and here it's a similar argument as before. So we first need to visit c and can then visit b in order to make sure that we visited every successor of b before visiting b itself and then finally as the last node we can visit our node a. So now let's go through this algorithm step by step and while doing this I will do two things. One is I will write down the num paths for each of the nodes that we are visiting and at the same time and for this I will use blue color I will write down the values that this algorithm is associating with the different edges of the graph. So the first node we are visiting is node f and looking at the algorithm this is the first of the two branches that we have here because node f is actually a leaf node and therefore we set num paths of f2,1. Next we are visiting node e. e is not a leaf node so we will go into the else branch and that means we start by initializing num paths with zero and then we are visiting each of the edges that are going out of e so there's only one in this case which goes from e to f and for this one we will set the value of the edge to the current num paths of n so the current num paths of e which means we set the value of this edge here oops wrong color to zero like this and then we are incrementing the current num paths of e by adding the num paths of the destination node of f which is one so we will set instead of having zero here we will put a one and this is the also the final num paths of this node e. Next we are visiting node d again d is not a leaf node so we will initialize its num paths value with zero and then go through all the edges of this node. This node d has two outgoing edges let's just first visit the one that goes from d to f which means we will associate with this edge the current value of of num paths of d so we will put a zero here and then after that we look at the second statement that we need to execute for every edge which is to increment the current num paths of the source node of d by adding the current num paths of the destination node of f which means we're adding one to this value here and then num paths of d is zero. Now we've only visited the first outgoing edge of d but there's also the other one that goes from d to e. Also for this one we look at the current num paths of d and use this as the value for this edge so we put a one here and then we again increment num paths of d with the num paths value of the destination node of e which means we will add another one here and at the end have num paths of d equal to two. Next we are visiting node c and here it's again not a leaf node so again we initialize num paths to one then visit one the one and only outgoing edge that c has which is this one and label it with the current num paths and then add the num paths of the destination so the num paths of d which is two to our current value which means num paths of c will at the end also be two. The next node to visit is node b again not a leaf node so we initialize num paths to zero and then go through the outgoing edges there are two of them let's say we are visiting the one that goes from b to c first that means we will set the value of this edge from b to c to the current num paths of b which is zero and after that increment num paths with the num num paths of c so this means we set this value to two and then look at the other edge where we label the edge with the current num paths of b meaning two and then add to the num paths of b the num paths of d which means this two here becomes a four and then finally there's only one node left to be visited and this is node a which again is not a leaf node again we initialize this num paths to zero and then go through the two outgoing edges where for the first one we'll set this values this edge here to zero and then increment the current num paths of a with the num paths of c so with we add two and then put this value as the edge value on the next edge and then add the num paths of b to num paths of a so we have two plus four which at the end gives us six and this is the final result that we get from this algorithm so now we have every edge associated with some number and if you now look at these edge numbers you'll see that each of them is chosen such that if you go through some path you'll get a unique number that identifies this path which is exactly what we wanted to have in the first part of the ball errors algorithm not to check if this idea was clear enough i'll have a little quiz for you which is a slightly different graph from the graphs that we've seen so far so it looks very similar to the one that we had before but the big difference is that there's no edge here between b and c and now what i would like here to do is to basically do the same algorithm that i've just done on this graph in order to compute the values that need to be associated with these edges so basically the question is what will be here and in order to do this you'll need to decide in which order to visit these nodes and what num path values to associate to associate with each of these nodes as a little hint i will tell you that the sum of the different num path values if you go through all nodes should be 12 and then another hint to for you to check whether your solution is correct is that once you have associated values with each of the edges in the graph you can go through all the four paths that this graph has and check if each of them has a different sum so that each path is labeled with some value between 0 and 3 so there should be a value a path value a path labeled with 1 a path labeled with 2 a path labeled with 3 and that should cover all all paths so at this point please stop the video for a moment and just try it out yourself to see if you've really understood it and if anyone doesn't really know the solution or isn't sure whether the solution is correct feel free to ask a question in alias all right so now we've seen the first part of this algorithm where we have computed an integer value for each of the edges such that the sum along a path yields a unique number for every path and now the second step of the algorithm wants to minimize the effort that is needed to increment values when edges are actually taken at runtime based on some information about how frequent these individual edges are actually executed so we now look into this second step and we'll see how the algorithm addresses that second problem for the second part of this algorithm we need to introduce some new terminology and this terminology is from the graph theory field and this is the idea of a spanning tree so given some graph g a spanning tree is essentially an undirected subgraph of the given graph g that is a tree and at the same time contains all nodes that we have in the graph g and then once you have found such a spanning tree there's another term namely the so-called chord edges and those are all the edges that are in our original graph g but that are not part of the spanning tree t now as an example to illustrate this idea of spanning trees let's just have a look at our graph that we've used as a running example throughout this lecture and this one will be our original graph g in this case so here it is again and now in addition to the edges that we've used all the time I'll also use the artificial back edge that we have introduced earlier when I talked about the assumption that the algorithm makes and now I'm giving you a couple of examples of potential spanning trees and the question for you is which of these examples is actually a spanning tree of g all right so here the examples there are five of them for each example I have not repeated the names of the nodes but just put these little circles but I guess you can basically see which node I mean and then between the nodes there are some edges and now the question is given the requirements that we have for a spanning tree which of these are actual spanning trees now let's have a quick look back at these requirements so essentially what we want to have here is that the spanning tree must be an undirected subgraph of g and it must be a tree and it must contain contain all nodes of the original graph so by given these requirements we can see that this first candidate is actually not a spanning tree simply because it does not contain this one node here so number one is not a spanning tree in contrast number two is a spanning tree so this one is fine because it does contain all nodes it is a tree and yeah it's an undirected subgraph of g number three does also contain all nodes but it's problematic because we have a circle here so this means this one is actually not a tree in contrast number four is fine because even though it looks a bit weird it is a tree and it contains all nodes of our graph and the same is true for candidate number five which is another spanning tree of our original graph g so as you can see here there's possibly more than one spanning tree of a given graph and each of them must include all the nodes of the original graph so now given this idea of a spanning tree we can use this to find a way to increment just a subset of all the edges of our graph so that we still have this property that the sum of the edges along a path gives you a unique encoding of the path and now the idea here is the following so we start by choosing among the different spanning trees of the graph that spanning tree that maximizes the edge costs so basically we look at the costs of the individual edges that would be part of a spanning tree and then to sum them and if this is the maximum across all spanning trees of the given graph then that's the spanning tree that we want to have now those are the edges that we do not want to instrument because they are the most expensive ones and instead what we do is we will then increment our our counter only at the chords of the spanning tree so exactly at those edges that are not part of the spanning tree and in order to yeah and then by doing this we basically get increments at exactly those places of the program that are executed the least often but where we still at the end get this nice property that the sum of the edge values is a unique encoding for the path through our graph so let me illustrate this idea again using our running example so here you again see our graph including the artificial back edge and now we make the assumption that we know the cost of individual edges so we have some idea of for example how often each of these edges is executed so let's assume we have these values as our known edge cost and then based on these known edge costs the next thing we can do is to think about the most expensive spanning tree of this given graph and if you do this and try out a couple of spanning trees you will find the following namely that this spanning tree so I'm basically labeling those edges that are part of the tree and I'm omitting all the others that this one is the one that maximizes the overall cost of the involved edges now using this most expensive spanning tree what we now want to do is to find minimal increments so we need to basically only increment some of the edges that are not expensive in this graph but first let's have a look at the increments that we had computed earlier and that are not necessarily the minimal ones so these non-minimal increments adjust what we had computed earlier so let me just go back a few slides so these blue values that we had computed here as the values of the edges this is what I'm now using again so I'm just putting them here onto our graph again so we had two two and one here and all the others are zero but now we do not want to make these increment operations on edges that are among the expensive ones so we do not want to make them on those red edges that you see on the left but instead we want to make them on the chord edges so those edges that are not part of this most expensive spanning tree so looking at the spanning tree this means we want to ideally increment on this edge on this edge on this edge and on that edge but not on the others so now in order to do this so to only have increment operations on these green chord edges we need to move around these increment operations while still preserving the property that every path has a unique encoding and the algorithm to do this is something that I will not show here in the lecture simply because yeah there's not enough time to really cover also that algorithm but just believe me that there is an existing algorithm that does it and essentially what it computes at the end are these minimal increments that look as follows so we will have some increment here which doesn't really increment anything so this is just zero we will put an increment of two here of four here and of one here and now you may recognize these numbers because this is exactly what I've shown you at the very beginning where we have said that this is actually the path encoding that we would like to get and where we had already seen at the beginning that this is indeed a path encoding that gives every path from a to f a unique number good so now you know how to compute these minimal increments in theory let's have a quick look at how this is typically implemented so how to actually instrument a program in order to then really compute the encoding for the path and to then also count how often each path has been executed so the basic idea is that at the entry into our control flow graph we initializing this variable r that stores the sum of the edge values that are executed and we initialize this value to zero and then on all edges that have an increment we add a piece of code that actually represents this increment by adding some value to our counter r and then at the exit of the entire graph we need to increment the count for this path by basically using the value of r that we have at the end as the index into this array count which stores the counts for the different paths in the program so that's the basic idea and now every basic idea can be optimized even further so there are two optimizations that one can do here one is that instead of incrementing sorry instead of initializing this value r at the very beginning where we just set it to zero we can actually save this effort and only initialize a 2d already incremented value at the first edge that is taken on a path so we do not initialize it to zero but initialize it to the first value that it gets on the corresponding edge and we can also optimize a little further at the end so instead of doing this increment at the very end we can combine this with the last increment of r that happens by basically putting this kind of instruction at this last edge through the graph where we increment both r with some value and then also our counter for the corresponding path so now you may wonder once we have this encoding and this array count that tells us for every encoding how often it has occurred how do we actually get the path so how do we regenerate the path from a given encoding and here the idea is the following so if we say that this sum this encoding is lowercase r then what we do is the following we use the edge values that we have from the first step of the algorithm so basically these non-minimal increments and then we started the entry of the graph with some helper value capital r equal to our sum lowercase r and then we go through the graph until we reach the exit and at every branch so basically every time we need to decide which way we go we take the edge that has the largest value associated with this edge that is still smaller or equal than our capital r and then just remove this value from our capital r and we do this until we reach the end and then we've taken a path and this is exactly the path that is represented by the encoding lowercase r so let's again illustrate this idea using the running example so what you see here is the graph that you've already seen all the time plus the non-minimal increments for these edges that we have computed earlier and now let's suppose that we have one of these encodings let's say value four and we want to know which path this encoding corresponds to so what we do is we have this helper variable capital r which we initialize with the encoding that we have so with four and then we start at the entry node of our graph and then whenever we have to decide between two paths like for example here whether we go to b or to c we use the edge that has the largest value that still fits into our helper value uppercase r so in this case this would be this edge so that means we basically go to from a to b and then the decrement uppercase r with the edge value so we this means we subtract two and have two remaining now we add b and here again we have to decide do we go to c or do we go to d and again we take that edge that has the largest value that still fits into our capital r here which means we will take the one that goes down to d which means our next node on the path is d and we again remove the value of this edge from our uppercase r meaning we have only zero left here and now when we are at edge d we in principle could again decide between going to e or to f but since we have only zero left here in our uppercase r the only choice that we have is to take the edge directly to f and this gives us the path that is associated with this encoding of four now just as a another example let's assume that our encoding is one then we do the same so we take this one as kind of the gas that we can use on our way to the exit now we are here and we cannot go to b because that would cost us two but we have only one left so what we do is instead to go to c now once we are at c there's only one option we go to d and now at d we have two options we could go down to e and this is also what we do but as a result we have to remove one from our remaining number so we have then e and from e there's only one way out which is two f so this is the path that corresponds to encoding one all right and this is the end of video number two in this lecture on path profiling so you've now seen the core of the ball narrows algorithm which is a way to address this path profiling problem that is both correct so it always gives a unique encoding for every path and efficient because it guarantees that the cost for the increment operations is minimal thank you very much for listening and see you in the next video