6.3.4 Complete cross-jumping and path determination algorithm (repeated merging)
If cross-jumped regions were never considered for further application of the cross-jumping algorithm, the path determination procedure described in the previous section would be adequate. However, not only can a single pass through the generated object code create multiply-merged regions, but the compiler applies the cross-jumping algorithm and other object code optimizations to the code stream repeatedly, until no changes occur. To keep track of the effects of these iterated optimizations, some additional static and dynamic information is needed.
6.3.4.1 Summary of the complete cross-jumping and path determination algorithm
The problems arise when a candidate sequence for a new merge (either the to-delete sequence or the to-remain sequence) contains either a portion of a previously merged region or a sentry of a previously merged region. Given a path determiner d, we refer to the determiner of its opposing sequence as d.
If a candidate sequence contains a portion of a previously merged region, the candidate sequence contains at least one instruction cell that already has more than one source descriptor. Each source descriptor has a determiner use for a previously merged sequence's path determiner in its pdToCheck field. The complete algorithm records the instruction cell's use of the new path determiner (for the candidate sequence) by allowing a single source descriptor to have multiple pdToCheck fields. That is, each source descriptor from the candidate sequence uses the candidate path determiner in addition to its previous path determiners. As before, the compiler merges the candidate sequence sourcelist with the sourcelist from the corresponding instruction cell in the opposing candidate sequence.
Figure 6-10 shows an instance of repeated merging. In the flowgraph form of the source fragment, the instructions are shortened to their first identifier. The numbers at the lower right of
fig67.press leftMargin: 1.875 in, topMargin: 1 in, width: 5.375 in, height: 8.5 in
Figure 6-10. Simple repeated cross-jumping.
an instruction letter refer to its source information: path determiner uses appear here as subscripts. An instruction's path determiner assignments appear at its upper left. For example, after the second merge, source descriptor 15 uses path determiners 1 and 4. The debugger interprets this information as follows: path determiners 1 and 4 must both have later timestamps than their opposing determiners (that is, path determiners 2 and 3) in order for statement 15 to be executing. All possible execution path traces for the final merged form, along with the path determiner assignments and source alternatives that they imply, are shown at the bottom of the figure.
Now consider the case in which a candidate sequence contains a sentry of a previously merged region; that is, the candidate sequence contains an instruction cell with a non-empty pdToSet list. It is tempting to suppose that the compiler could remove these path determiner assignments from their current location and place a copy of them at each sentry of the candidate sequence. By this action, the compiler would represent the fact that the previously merged code sequences actually merge earlier than was known before. Unfortunately, there are programs for which this strategy fails to provide full path determination. These programs are characterized by multiple control-flow paths that are wholly contained in a merged region. Figures 6-11A and 6-11B show the correct repeated cross-jumping of such a program. To see how the proposed handling of embedded sentries would fail, suppose that the fourth cross-jumping step (in Figure 6-11B) were to move determiner assignments 1, 2, and 5 up to the location of determiner assignment 7, and similarly move determiner assignments 3, 4, and 6 up to the location of determiner assignment 8. It would then be impossible for the debugger to distinguish between the execution of statements 25 and 27, or between statements 34 and 36.
Instead, the instruction's sentry information is made to depend upon the candidate path determiner, in much the same way as a source descriptor is made to depend upon the path determiner for the sequence that contains it. To record the fact that a path determiner assignment depends on the value of the new candidate determiner, each of the instruction's sentry descriptors is given an indirect determiner use for the candidate path determiner. As a result, the debugger will collect timestamp values for the two opposing determiners at runtime, and will check these values to determine on behalf of which of the two sentry descriptors the instruction is executing. Finally, if the candidate sequence is the to-delete sequence of this merge, the algorithm adds the updated sentry information to the current cell in the to-remain sequence.
fig68a.press leftMargin: 1.9375 in, topMargin: 1 in, width: 5.625 in, height: 8.5 in
Figure 6-11a. Repeated cross-jumping with embedded control-flow, part 1.
fig68b.press leftMargin: 1.625 in, topMargin: 1 in, width: 5.875 in, height: 8.5 in
Figure 6-11b. Repeated cross-jumping with embedded control-flow, part 2.
To see that this algorithm works, suppose that an object location x has a possible path determiner assignment d3 whose pdToCheck list contains (only) d1. At runtime, the execution of x corresponds to a sentry d3 of an inner merged region if and only if the outer merged region was entered via path determiner d1 more recently than it was entered via path determiner d1.
Dealing with the added complexity of repeated cross-jumping also requires some runtime modifications. The following path determiners are activated as a result of a breakpoint request: first, all path determiners that are used by any source alternative for the breakpoint's object location, and second, (recursively) for any path determiner d that is indirectly used by a determiner activated in step 1, both d and d.
Timestamp checking at a primary breakpoint must also be altered slightly: a list of determiner uses of the form (
d
1,
d
2) on either a single source descriptor or a single sentry descriptor is satisfied if and only if
latest timestamp (d1) e latest timestamp (d1)
and
latest timestamp (d2) e latest timestamp (d2).
Ascertaining the latest timestamp value for a single path determiner at a primary breakpoint also reflects determiner nesting: a timestamp value in a determination cell is valid only if the latest timestamp value of the determiners that it uses is not earlier than the latest timestamp value of their opposing determiners.
These timestamp-checking algorithms thus need to be able to find the opposing determiner for any determiner. A simple scheme, which works as long as cross-jumping introduces exactly two determiners for each merge, is to use an even/odd distinction.
Notice that the order of consideration of joining paths for cross-jumping can affect the final placement and number of path determiners. Figure 6-12 shows the same program as Figure 6-10, but a different cross-jumping ordering has caused more determiners and more complex runtime behavior. (Figure 6-12 has indirect determiner uses, while the earlier figure did not need them.)
We now show the full cross-jumping and path determination algorithm. Changes from the original Cedar algorithm are italicized; changes from Algorithm NCJ1 are italicized and underlined.
fig69.press leftMargin: 1.8125 in, topMargin: 1 in, width: 5.25 in, height: 8.5 in
6.3.4.3 Step
2B1c1a2: Keeping the joining jump's sentry information
As in the case of keeping the joining jump's source information [step 2B1c1a1; see Section 6.4.2.4], keeping the joining jump's sentry information is only a problem when the joining jump has no corresponding instruction on the to-remain path—that is, when the to-remain path is a fall-through path. These two problems are similar, but not identical. The joining jump's source information must be kept only when the joining jump represents the start of execution of a source statement, so as to permit a statement breakpoint there. Furthermore, since a statement breakpoint is visible to the user, the new instruction holding the source information must execute in the proper order with respect to its surrounding instructions: that is, after the bottom instruction of the merged sequence and before the instruction following the joining label.
In contrast, the joining jump's sentry information must always be kept, so as to properly distinguish among merged instructions that execute after it. Since a determining breakpoint is invisible to the user, there is a bit more flexibility in its placement, but we would like it to fall on the new sentry of the previously-merged region. [Recall that the sentry instruction for a merged region is always an immediate dynamic predecessor of the region.] If the joining jump is a sentry for a previously-merged region, the joining jump is the immediate dynamic predecessor of a previously-merged instruction. Since the joining jump is an unconditional jump, that merged instruction can only be the instruction following the joining label. So the new sentry instruction that will take the joining jump's place is the new immediate dynamic predecessor of that merged instruction after the current merge. If step 2B1c1a1 inserted a null instruction to hold the joining jump's source information, the new immediate dynamic predecessor is that null instruction when it executes on behalf of the to-delete sequence—that is, qualified by the to-delete path determiner. If no null instruction was inserted (and the source information was discarded), the new immediate dynamic predecessor is the bottom instruction of the new merge, qualified by the to-delete path determiner.
Moving the joining jump's path determiner assignments backward in this way could include more execution flow paths to the previously-merged region than the joining jump did originally. Because the bottom instruction of the to-delete sequence is a matching instruction and is therefore neither a label nor an unconditional jump, that bottom instruction is guaranteed to be the single immediate dynamic predecessor of the joining jump. However, under unusual circumstances, the bottom instruction might be a conditional jump into the same previously-merged region, in which case the bottom instruction would be marked with the opposing determiner of one that marks the joining jump. Figure 6-13 illustrates this situation. In this unusual case, moving the joining jump's path determiners back would create ambiguity: when control comes through the bottom instruction, if the to-delete determiner of the new merge is true, then both the to-delete and the to-remain determiners of the previously-merged region are true. To avoid this ambiguity, solutions similar to the proposed solutions for the joining jump's source information can be used.
To retain expected behavior:
1. Insert a null instruction on the to-remain path in this case also. This would make the program slightly slower, but hopefully smaller.
2. Abort the cross-jump of the new region. The resulting program would be larger than the fully optimized version, but the same speed as the unoptimized version.
To fall back to truthful behavior:
3. Continue with cross-jumping, moving the joining jump's path determiners back to mark the bottom instruction in the to-delete path.
fig60a.press leftMargin: 1.625 in, topMargin: .9375 in, width: 5.6875 in, height: 8.5 in
Figure 6-13. Keeping the joining jump's sentry information.
The Navigator compiler uses solution 1 because it matches the chosen solution for the joining jump's source information.
Would these unusual circumstances ever arise? It seems very unlikely. The limited form of GOTO statements in Cedar ensures that the source version of a Cedar program can never pose this problem. But it is quite difficult to predict the effects of repeated applications of cross-jumping and other object code optimizations. Because I cannot prove that situations like this do not arise from repeated optimization, the Navigator compiler must be prepared to handle them correctly.