NCL 406: Coalescing Instructions

So far, most of our programs have placed one instruction on each source line:

MOVE r0 10
ADD r0 r0 5
D.TXT r0
D.BLT

This makes programs easy to read while we're learning what each instruction does.

NCL does not require it.

Several instructions can share the same physical source line, separated by semicolons:

MOVE r0 10; ADD r0 r0 5; D.TXT r0; D.BLT

This is called coalescing.

The instructions are still separate instructions. They execute from left to right just as they would on separate lines.

Coalescing changes how we arrange our source, not what those instructions mean.

Building a Source Line

We don't need to coalesce everything that happens to fit.

Consider some familiar coordinate calculations:

MOD #column #selection 3
DIV #row #selection 3

MUL #x #column 4
ADD #x #x 11

MUL #y #row 2
ADD #y #y 2

Each pair performs one small piece of work.

We can make those relationships visible in the source:

MOD #column #selection 3; DIV #row #selection 3
MUL #x #column 4; ADD #x #x 11
MUL #y #row 2; ADD #y #y 2

We could put all six instructions on one line:

MOD #column #selection 3; DIV #row #selection 3; MUL #x #column 4; ADD #x #x 11; MUL #y #row 2; ADD #y #y 2

But we've lost some useful structure.

A good general rule is:

Coalesce by unit of thought, not maximum density.

A source line can represent a small piece of work rather than merely as many instructions as we can squeeze onto it.

Definitions Can Be Coalesced Too

Constant and alias definitions can share source lines in the same way:

#empty 0; #cross 1; #nought -1

Related aliases can also be grouped:

#selection r0
#column r1; #row r2
#x r3; #y r4

Definitions on a coalesced line need semicolons between them, just like instructions.

The same is true when a label shares a line with an instruction:

$again; INC r0; BLT $again r0 10

The semicolon after $again separates the label from the first instruction.

One Entry Point

There is an important difference between several instructions on separate source lines and several instructions coalesced onto one.

A physical source line has one entry point: its beginning.

Consider:

$again; INC r0; D.TXT r0; BLT $again r0 10

When BLT branches to $again, execution returns to the beginning of that physical source line.

It cannot branch directly to:

D.TXT r0

in the middle of the line.

There is no way to use a label or branch destination to enter partway through a coalesced source line.

This is one reason to think about which instructions belong together. Coalescing them doesn't turn them into one instruction, but it does place them behind the same entry point.

Arranging Work on a Line

When a source line contains control flow, a useful ordering is:

guaranteed work → conditional branch → conditional work → unconditional branch

Most source lines will contain only some of those parts.

For example:

INC r0; BLT $again r0 10

INC is guaranteed to happen.

Then BLT decides whether execution should leave the line.

Or:

ADD r0 r1 r2; BGT $large r0 100; MUL r0 r0 2; JUMP $done

Read it from left to right:

  1. ADD performs guaranteed work.
  2. BGT may branch away.
  3. MUL executes only if BGT was not taken.
  4. JUMP unconditionally leaves the line.

The arithmetic after BGT is deliberately placed there.

Because the branch is conditional, execution may fall through to the instructions that follow it. Those instructions therefore become conditional work of their own.

Here's a particularly practical example:

BEQ $done r1 0; DIV r0 r0 r1

If r1 is zero, execution branches to $done.

Otherwise, execution continues through the line and performs the division.

Putting the arithmetic after the branch makes the relationship clear: the division happens only when the divisor is not zero.

Our ordering is a guideline for expressing the flow of a source line, not a rule that arithmetic must always appear before branches.

Unconditional Branches End the Line

A conditional branch may fall through.

An unconditional branch cannot.

JUMP, CALL, and RET all unconditionally leave the current source line. Any instruction placed after one is unreachable:

JUMP $somewhere; INC r0

INC r0 can never execute.

The same is true here:

RET; D.TXT "Done"

Once RET executes, control has left the line.

This is why an unconditional branch belongs at the end of any useful coalesced line:

ADD r0 r1 r2; BGT $large r0 100; MUL r0 r0 2; JUMP $done

There is no useful place after the JUMP.

CALL does not return to the middle of a line

CALL always stores the next physical source line as its return destination.

This means instructions after a CALL on the same source line are unreachable:

ncl PUSH r0; CALL $work; POP r0

When $work executes RET, execution does not resume at POP r0. It resumes at the beginning of the next physical source line.

Write the call at the end of its line instead:

ncl PUSH r0; CALL $work POP r0

More generally, JUMP, CALL, and RET all unconditionally leave the current source line. Nothing after them on that line can execute.

This makes source-line boundaries particularly important around subroutine calls.

For example:

PUSH r0; PUSH r1; CALL $work
POP r1; POP r0

The first line has one purpose: prepare for and make the call.

The second has another: restore state after the call returns.

Short Subroutines

Coalescing can make small subroutines pleasantly compact.

Suppose a routine receives one value, doubles it, and returns the result:

$double
POP r0
MUL r0 r0 2
PUSH r0
RET

We could write the entire routine on one line:

$double; POP r0; MUL r0 r0 2; PUSH r0; RET

For something this small, that's still fairly easy to follow.

We could also preserve more visual structure:

$double; POP r0; MUL r0 r0 2
PUSH r0; RET

Both are valid.

Which is better depends on what makes the routine easiest to understand.

Again:

Coalesce by unit of thought, not maximum density.

Coalescing a Loop

Loops can also fit naturally onto a source line.

Here's an expanded loop:

#value r0
#limit 10

MOVE #value 0

$again
INC #value
D.TXT #value
D.TXT " "
BLT $again #value #limit

D.BLT

We can begin by grouping the definitions:

#value r0; #limit 10

Then the loop itself:

$again; INC #value; D.TXT #value; D.TXT " "; BLT $again #value #limit

The complete program becomes:

#value r0; #limit 10

MOVE #value 0

$again; INC #value; D.TXT #value; D.TXT " "; BLT $again #value #limit

D.BLT

The loop now forms one source line and one entry point.

Every time BLT branches to $again, execution begins again with INC.

That line is getting fairly busy, though.

We could instead write:

$again; INC #value; D.TXT #value; D.TXT " "
BLT $again #value #limit

This version may be easier to scan.

Both versions perform the same work.

For now, choosing between them is mostly a question of how we want to organize the source.

We'll discover another consideration in the next lesson.

Notecards have a line-length limit

When an NCL program is loaded from a Second Life notecard, each physical source line is limited to 1024 bytes.

Lines longer than this are truncated when Second Life reads the notecard. Heavy coalescing can therefore remove instructions from the end of a line, or cut the line in the middle of an instruction.

Programs already stored on an NCS/e disk are not subject to this notecard line limit.

In practice, there is rarely a good reason to make a coalesced line anywhere near this long. Coalesce related work, not entire programs.

Try It

Start with this expanded program:

#value r0
#limit 10

MOVE #value 0

$again
INC #value
MUL r1 #value 2
D.TXT r1
D.TXT " "
BLT $again #value #limit

D.BLT

Coalesce related operations while keeping the program easy to follow.

Try grouping the definitions, then decide how much of the loop belongs on one source line.

Remember:

guaranteed work → conditional branch → conditional work → unconditional branch

There isn't one required arrangement. The goal is to make each source line represent a useful unit of thought without hiding the program's control flow.


Coalescing lets us decide what a physical source line represents. Several instructions can form one useful unit, but that line still has only one entry point.

On NCS/e, source-line boundaries aren't only visible to the programmer. They also matter to the way programs execute.

In the next lesson, NCL 407: Optimizing Execution, we'll use that fact deliberately.