Explainer 8 of 11

Rounds across programs, and walking only what changed

Read first: Area need, Local site resolution

A program's need on entry can be decided by a program two calls away, whose answer depends on others in turn. So one walk per program is not enough: the corpus is walked in rounds until no answer moves, and on about 3,800 programs, walking every one every round is the cost.

PROGA LINKs to PROGB, which LINKs to PROGC; then PROGA XCTLs to the name it reads back from bytes [0, 8). So PROGC needs those bytes on entry, though its own code only writes them. Five rounds settle it, and walking only the programs whose inputs changed takes 9 walks instead of 20.

Each program is a member, with an entry row (bytes it needs on entry) and a back row (bytes its callers read after it returns).

The example

Four programs share one 16-byte commarea layout. Besides the PROGA → PROGB → PROGC chain, PROGD reads a program name from a queue and XCTLs there, handing its commarea on. Solid arrows: LINKs to a named program; dashed: XCTLs to a variable.

flowchart TD
    A[PROGA] -->|"LINK, WS-AREA"| B[PROGB]
    B -->|"LINK, DFHCOMMAREA"| C[PROGC]
    A -.->|"XCTL WS-NEXT-PGM, no area"| Q1["?"]
    D[PROGD] -.->|"XCTL WS-NEXT, DFHCOMMAREA"| Q2["? (read from NEXTQ)"]

The strip below is the 16-byte commarea as the rounds leave it. PROGB, PROGC and PROGD each need bytes [0, 8), CA-NEXT-PGM, on entry (marked need). Bytes [8, 16), CA-CODE and CA-ACCT, are needed by nobody, so a fixpoint narrowed to this need does not have to track them.

0
1
2
3
4
5
6
7
CA-NEXT-PGM
need
need
need
need
need
need
need
need
8
9
10
11
12
13
14
15
CA-CODE
CA-ACCT
·
·
·
·
·
·
·
·

That is the whole answer, one row per member, settled after 5 rounds and 9 walks:

PROGA: receives nothingPROGB: entry [0, 8), back [0, 8)PROGC: entry [0, 8), back [0, 8)PROGD: entry [0, 8), back {}

The lines that matter. CA-NEXT-PGM and PROGA's WS-NEXT-PGM are both bytes [0, 8):

* PROGA
EXEC CICS LINK PROGRAM('PROGB')
     COMMAREA(WS-AREA) END-EXEC.
EXEC CICS XCTL PROGRAM(WS-NEXT-PGM) END-EXEC.
* PROGB
EXEC CICS LINK PROGRAM('PROGC')
     COMMAREA(DFHCOMMAREA) END-EXEC.
EXEC CICS RETURN END-EXEC.
* PROGC
IF EIBAID = DFHPF3
    MOVE 'PROGE' TO CA-NEXT-PGM
END-IF.
EXEC CICS RETURN END-EXEC.
* PROGD
EXEC CICS READQ TS QUEUE('NEXTQ')
     INTO(WS-NEXT) END-EXEC.
EXEC CICS XCTL PROGRAM(WS-NEXT)
     COMMAREA(DFHCOMMAREA) END-EXEC.
Full listings (all four programs and the shared layout)
      *> Layout shared by PROGB, PROGC, PROGD (PROGA uses WS-AREA, same shape)
       01  DFHCOMMAREA.
           05  CA-NEXT-PGM          PIC X(8).
           05  CA-CODE              PIC X(2).
           05  CA-ACCT               PIC X(6).

       PROGRAM-ID. PROGA.
       WORKING-STORAGE SECTION.
       01  WS-AREA.
           05  WS-NEXT-PGM          PIC X(8).
           05  WS-CODE              PIC X(2).
           05  WS-ACCT               PIC X(6).
       PROCEDURE DIVISION.
           EXEC CICS LINK PROGRAM('PROGB') COMMAREA(WS-AREA) END-EXEC.
           EXEC CICS XCTL PROGRAM(WS-NEXT-PGM) END-EXEC.

       PROGRAM-ID. PROGB.
       PROCEDURE DIVISION.
           EXEC CICS LINK PROGRAM('PROGC') COMMAREA(DFHCOMMAREA) END-EXEC.
           EXEC CICS RETURN END-EXEC.

       PROGRAM-ID. PROGC.
       PROCEDURE DIVISION.
           IF EIBAID = DFHPF3
               MOVE 'PROGE' TO CA-NEXT-PGM
           END-IF.
           EXEC CICS RETURN END-EXEC.

       PROGRAM-ID. PROGD.
       WORKING-STORAGE SECTION.
       01  WS-NEXT                  PIC X(8).
       PROCEDURE DIVISION.
           EXEC CICS READQ TS QUEUE('NEXTQ') INTO(WS-NEXT) END-EXEC.
           EXEC CICS XCTL PROGRAM(WS-NEXT) COMMAREA(DFHCOMMAREA) END-EXEC.

What the walks need to know about these programs:

The callee map and its inversion: PROGB in PROGA's callees becomes PROGA in PROGB's callers.

membercallees (named or resolved)callers()
PROGA{PROGB}—
PROGB{PROGC}{PROGA}
PROGC{}{PROGB}
PROGD{} (one unresolved site)—

Members with an unresolved site that hands over an area: {PROGD}.

Rows below are commarea byte ranges, written half-open: [0, 8) is bytes 0 to 7, the eight bytes of CA-NEXT-PGM.

Step by step

Each step is one round. It shows which members were walked, the table after the round (amber: changed in this round), and the members dirtied for the next round with the reason.

Rounds until the table settles (5 rounds, 9 walks)
  1. Every member starts needing nothing. The dynamic need is the union of the entry rows, so it is empty too. Every member is dirty: the first round walks them all.

    memberentryback
    PROGA{}{}
    PROGB{}{}
    PROGC{}{}
    PROGD{}{}
    dynamic{}

    To walk

    PROGAPROGBPROGCPROGD
  2. Walk all four against the empty table. Only PROGA finds anything: its XCTL reads [0, 8) after the LINK to PROGB, so it reports reading [0, 8) back from PROGB. That becomes PROGB's back row. No entry row changed, and the dynamic need did not change.

    memberentryback
    PROGA{}{}
    PROGB{}[0, 8)
    PROGC{}{}
    PROGD{}{}
    dynamic{}

    To walk next — PROGB: its own back changed

    PROGB
  3. Walk PROGB. Its RETURN now demands [0, 8), which is live back to the LINK. So PROGB reads [0, 8) back from PROGC. The LINK writes the area, so nothing reaches PROGB's entry: its entry row stays empty. PROGA, PROGC and PROGD keep their round-1 reports.

    memberentryback
    PROGA{}{}
    PROGB{}[0, 8)
    PROGC{}[0, 8)
    PROGD{}{}
    dynamic{}

    To walk next — PROGC: its own back changed

    PROGC
  4. Walk PROGC. Its RETURN demands [0, 8). The MOVE covers those bytes on one path only, so they are live on entry. PROGC's entry row changed: its callers, {PROGB}, are dirty. The union of entry rows grew, so the dynamic need changed: every member with an unresolved site, {PROGD}, is dirty.

    memberentryback
    PROGA{}{}
    PROGB{}[0, 8)
    PROGC[0, 8)[0, 8)
    PROGD{}{}
    dynamic[0, 8)

    To walk next — PROGB: caller of PROGC, whose entry changed; PROGD: dynamic need changed

    PROGBPROGD
  5. Walk PROGB and PROGD, both against the round-3 table. PROGB's LINK must now supply PROGC's entry need, so [0, 8) is live on entry. PROGD's unresolved XCTL must supply the dynamic need, so it too needs [0, 8) on entry. PROGB's entry changed: its caller PROGA is dirty. PROGD's entry changed, but nothing calls it. The union is still [0, 8), so the dynamic need did not change. No back row changed.

    memberentryback
    PROGA{}{}
    PROGB[0, 8)[0, 8)
    PROGC[0, 8)[0, 8)
    PROGD[0, 8){}
    dynamic[0, 8)

    To walk next — PROGA: caller of PROGB, whose entry changed

    PROGA
  6. Walk PROGA. Its LINK now demands [0, 8) of WS-AREA, but PROGA receives no area, so it has no entry row to grow. It still reads [0, 8) back from PROGB. Nothing changed, so nothing is dirty. The rounds stop.

    memberentryback
    PROGA{}{}
    PROGB[0, 8)[0, 8)
    PROGC[0, 8)[0, 8)
    PROGD[0, 8){}
    dynamic[0, 8)

    To walk next

    none — settled

Walking every member every round produces the same five tables, round for round, because a skipped member would have reported exactly what it reported last time. In the grid under why only some members walk again, below, that is all 5 × 4 = 20 cells. The dirty sets walk only the amber ones: 4 + 1 + 1 + 2 + 1 = 9.

Notice the direction each fact travels, on the row graph in the problem. A back row moves down (PROGA → PROGB → PROGC): the amber back cells, rounds 1 and 2. An entry row moves up (PROGC → PROGB → PROGA): the amber entry cells, rounds 3 and 4. The dynamic need moves sideways to PROGD, through that graph's dynamic box.

With --max-rounds 3, the run stops after round 3 and warns that it stopped after 3 rounds with 2 members still to walk. It writes the walk's round-3 table (step 3), compared here with the settled one: PROGB and PROGD need nothing on entry. Those bytes arrive only through the longer chain PROGA → PROGB → PROGC and back, which three rounds did not finish carrying.

memberentry, --max-rounds 3entry, settled
PROGA{}{}
PROGB{}[0, 8)
PROGC[0, 8)[0, 8)
PROGD{}[0, 8)

amber: short of the settled need. The back rows, [0, 8) for PROGB and PROGC, are already settled by round 3.

Why only some members walk again

The walk started every member at "needs nothing" and walked each round against the table the round before built. Rows only grow, and they are bounded by the size of the areas, so eventually a round changes nothing: step 5. That table is the least one that holds, the strip in the example.

Round 1 walked all four members; after that, each round walked only one or two. Skipping is safe because a walk reads exactly three things from the table:

  1. the entry rows of the programs its call sites reach;
  2. its own back row;
  3. if it has a call site that hands over an area and that nothing resolved, the union of every member's entry row (the dynamic need).

For the example, one column per item in that list:

member1. entry rows of2. own back3. dynamic?
PROGAPROGBPROGA'sno: its XCTL hands over no area
PROGBPROGCPROGB'sno
PROGC—PROGC'sno
PROGD—PROGD'syes: its XCTL

Everything else a walk reads is its own program, which never changes. So if none of the cells in a member's row of that table changed since its last walk, walking it again gives the same report: keep that report and skip the member. That leaves three reasons to walk a member again: a program it calls got a new entry row, its own back row changed, or the dynamic need changed and it has an unresolved site. Each fired in the walk:

reasonin the walk
callee's entry changedround 3: PROGC's entry becomes [0, 8), so its caller PROGB walks in round 4. Round 4: PROGB's entry becomes [0, 8), so PROGA walks in round 5
own back changedround 1: PROGB's back becomes [0, 8), so PROGB walks in round 2. Round 2: PROGC's back, so PROGC walks in round 3
dynamic changed (member has an unresolved site)round 3: the dynamic need goes from {} to [0, 8), so PROGD walks in round 4

The first reason needs "who calls PROGC", but each member only knows what it calls. So the callee map is inverted once, before the first round: "member → programs its sites reach" becomes "program → members whose sites reach it", the callers() column of the callee map in the example. The sites counted are those naming a program, plus dynamic sites resolved to one or more programs (in their own program, see local site resolution, or by an earlier combine).

This is the worklist idea one level up. There, the items are statements, and a statement whose notes changed queues its neighbours. Here, the items are members, and a row that changed queues the members that read it. One difference: within a program the worklist takes one statement at a time and its result is visible at once. Here a whole round walks against the table the previous round built: in the grid below, round 4 walks PROGB and PROGD both against round 3's table. That is what lets one round's walks run in parallel.

roundPROGAPROGBPROGCPROGD
1walkwalkwalkwalk
2keptwalkkeptkept
3keptkeptwalkkept
4keptwalkkeptwalk
5walkkeptkeptkept

amber: walked this round; kept: last report reused

Why one walk per program is not enough

In step 1 of the walk, PROGB's walk found nothing, because the table it walked against was empty. Only after PROGA's walk filled PROGB's back row could PROGB's walk in step 2 find anything. A member's walk is ordinary backward liveness over its own statements (see area need), but the demand it starts from comes from the table. The right-hand column is PROGB's rows once the rounds have settled:

rowmeansPROGB
entrythe bytes of each area it receives that it needs on entry[0, 8)
backthe bytes of that area its callers read again after it returns[0, 8)

The fragment below, from PROGB and PROGD, marks where each demand comes in:

* PROGB
EXEC CICS LINK PROGRAM('PROGC')
     COMMAREA(DFHCOMMAREA) END-EXEC.
                  *> PROGC's entry row
EXEC CICS RETURN END-EXEC.
                  *> PROGB's own back row
* PROGD
EXEC CICS XCTL PROGRAM(WS-NEXT)
     COMMAREA(DFHCOMMAREA) END-EXEC.
                  *> the dynamic need

At the LINK, which hands over an area, PROGB must supply what the callee reads on entry: the callee's entry row. At its own EXEC CICS RETURN (or a GOBACK), it must supply what its callers read back: its own back row. At PROGD's XCTL, a dynamic call nothing resolved, it must supply what any member might read: the union of every member's entry row.

So each walk's output depends on rows that other walks produce. The graph below draws that for the example: an arrow runs from the walk that produces a row to the walk that reads it. PROGB has two arrows in (PROGC's entry row, its own back row from PROGA) and three out (PROGC's back row, its own entry row to PROGA and to the dynamic box). Run once, per-program liveness cannot settle a loop like the PROGB ⇄ PROGC pair, where each walk's output is the other's input: in the walk, PROGB fed PROGC's back row in step 2 and read PROGC's entry row back in step 4.

flowchart TD
    A[PROGA] -->|"PROGB's back"| B[PROGB]
    B -->|"PROGC's back"| C[PROGC]
    C -->|"PROGC's entry"| B
    B -->|"PROGB's entry"| A
    D[PROGD]
    B -->|"entry"| DYN["dynamic need = ⋃ entry"]
    C -->|"entry"| DYN
    D -->|"entry"| DYN
    DYN -->|"unresolved XCTL"| D

Where this shows up

The goal on a large CICS corpus (about 3,800 programs) was to resolve dynamic call targets: CALL WS-PGM, EXEC CICS XCTL PROGRAM(WS-NEXT-PGM). To do that cheaply, the fixpoint should track only the handed-over bytes some target can depend on. Area need computes those bytes per program, and these rounds are how it settles across the corpus. On CardDemo, the entry need summed over every program fell from 4,394 bytes to 92 once LENGTH OF bounds were folded and more sites resolved (area need has the steps), and the fixpoint narrowed to it gave identical targets, links and graph.

A commarea is a conversation between programs: no single program's source says that PROGC needs bytes [0, 8). And walking 3,800 programs per round is the cost, though most programs' inputs do not change after the first round or two: walking only the members whose inputs changed gives the same table for a fraction of the walks, 9 instead of 20 on the example.

The rule

T₀ = every member needs nothing; dirty₀ = every member
round k: walk each member in dirty₍k−1₎ against T₍k−1₎; keep every other member's last report
T_k = resolve(all reports): entry = own report; back = what callers read back
      (plus what unresolved calls read back); dynamic = ⋃ entry
dirty_k = callers(members whose entry changed)
        ∪ members whose back changed
        ∪ (members with an unresolved site, if dynamic changed)
stop when dirty_k is empty, or when k = --max-rounds (N > 0), warning |dirty_k|

Round 3 of the walk (step 3) is the rule applied once, term by term:

termround 3
walked: dirty₂{PROGC}, against T₂
members whose entry changed{PROGC}
callers of those{PROGB}
members whose back changed{}
dynamic changed?yes, {} → [0, 8): add {PROGD}
dirty₃{PROGB, PROGD}

The stop clause fires twice on this page: in round 5, where dirty₅ is empty (the last step of the walk), and with --max-rounds 3, at k = 3 with |dirty₃| = 2 — the table above.

Why this reaches the same rows as walking everything: a walk reads only its program, which is fixed, and the three things in the table under why only some members walk again. A member is left out of a round only when none of those changed since its last walk. So its kept report is the one a fresh walk would produce, and each T_k equals the table the walk-everything run builds in round k. PROGA's column in the grid shows it: walked in round 1, kept through rounds 2–4, walked again in round 5. Its two reports, from the walk:

PROGA's reportround 1 (walked)rounds 2–4round 5 (walked)
reads back from PROGB[0, 8)kept[0, 8)
entry rownone: receives no areakeptnone: receives no area

The round-5 walk changed nothing, which is why it is the last. It stops because rows only grow — more demand in a liveness walk never means fewer live bytes — and every row is bounded by the bytes of the areas: every amber cell in the walk goes from {} to [0, 8), and none ever shrinks.

What goes wrong

In cobble

The command is cobble-dataflow-area-need. Before the first round it inverts the callee map once. After each round it compares the table before and after and finds the three kinds of dirty member, and the next round walks only those. It stops when that set is empty, or after --max-rounds rounds with a warning. A walk reads, for each call, the callee's entry row, the dynamic need for an unresolved site, or every byte for an unknown callee; and for itself, its own back row. Run on this page's four programs with --max-workers 1, it logs the walk above round for round (timestamps, the reaching_defs and loading lines, and the closing "written to" line are left out):

area need: PROGD MAIN-PARA line 14 XCTL unresolved: not-a-literal
area need round 1: [1/4] PROGA, 0 bytes on entry
area need round 1: [2/4] PROGB, 0 bytes on entry
area need round 1: [3/4] PROGC, 0 bytes on entry
area need round 1: [4/4] PROGD, 0 bytes on entry
area need round 1: walked 4 members; 0 bytes needed on entry, 8 read back, summed over 4 members; 1 to walk next
area need round 2: [1/1] PROGB, 0 bytes on entry
area need round 2: walked 1 members; 0 bytes needed on entry, 16 read back, summed over 4 members; 1 to walk next
area need round 3: [1/1] PROGC, 8 bytes on entry
area need round 3: walked 1 members; 8 bytes needed on entry, 16 read back, summed over 4 members; 2 to walk next
area need round 4: [1/2] PROGB, 8 bytes on entry
area need round 4: [2/2] PROGD, 8 bytes on entry
area need round 4: walked 2 members; 24 bytes needed on entry, 16 read back, summed over 4 members; 1 to walk next
area need round 5: [1/1] PROGA, 0 bytes on entry
area need round 5: walked 1 members; 24 bytes needed on entry, 16 read back, summed over 4 members; 0 to walk next

The "to walk next" counts are the sizes of the chip rows in the walk: 1, 1, 2, 1, 0. The byte sums add up the table's columns: in round 4, entry rows of 8 bytes each for PROGB, PROGC and PROGD make 24, and back rows for PROGB and PROGC make 16.

The members are shared out among worker processes (--max-workers). Each worker holds its share of the members for the whole run; only the table and the reports cross between processes. One progress count, under a lock, is advanced by every worker as it finishes a load or a walk, so the log numbers members in the order they finish. It is never reset: each round subtracts the walks made before it to print [i/n] for that round, which is why round 4 in the log above counts [1/2], [2/2] rather than carrying on from round 3.

The walk inside each member is backward liveness: the same worklist idea as fixpoints, run backwards, as area need describes. What the rows mean, and where the demand comes from — the annotated fragment in the problem — is in area need.