The Let's Play Archive

EXAPUNKS

by Carbon dioxide

Part 10: SFCTA Highway Sign

Part 10 - SFCTA Highway Sign


=== Trash World Inbox ===

silentsnack posted:

This one has a some reasonable optimizations and one really stupid trick. First up to reduce file size you can simply test every single value instead of skipping over the ones you know expect to return false, at the expense of wasting a lot of cycles. Also instead of having to COPY F M to transmit the ID which you'll need on multiple EXAs and necessitates another COPY M X operation, you can just COPY F X and then REPL

code:
GRAB 300
COPY F X
LINK 800
REPL B
WIPE
GRAB 201
LINK 801
SEEK 9999
COPY #DATE F
LINK -1
COPY X F
COPY M F
COPY M F
MARK B
GRAB 200
MARK LOOP
TEST X = F
FJMP LOOP
COPY F M
COPY F M
SEEK -2
COPY 0 F
COPY 0 F
Brings it own to 99 cycles / 23 size / 3 activity
Ah. Of course just testing everything would reduce the size. I didn't think of that because my mind was still in "reducing cycles" mode I guess.

silentsnack posted:

And for reducing the cycle count... think of the silliest thing you could try to make a program run faster, and see how it compares to what you're about to see. First here's something with generally the same general function+structure as your 58 cycle solution, but uses loop unrolling and TJMP spam to make the file search run a bit faster
code:
;====XA====
LINK 800
GRAB 200
COPY M X
MARK LOOP
@REP 8
TEST X = F
TJMP FOUND
SEEK 2
@END
JUMP LOOP
MARK FOUND
COPY F M
COPY F M
SEEK -2
COPY 0 F
COPY 0 F

;====XB====
GRAB 300
COPY F X
COPY X M
LINK 800
WIPE
GRAB 201
LINK 801
SEEK 9999
COPY #DATE F
LINK -1
COPY X F
COPY M F
COPY M F
57/48/4 which is a slight improvement over 58 cycles.
So, loop unrolling does work. What's tricky is that when you unroll a loop that needs a jump anyway, the only way to save cycles, is to move some instruction out of the loop somehow. You did this by putting the TJMP just before the SEEK 2, making that final SEEK 2 and its followup SEEK -2 unnecessary. Apparently that turns out to save one cycle overall.

silentsnack posted:

And now for the same solution but a couple of lines changed to use a slightly different routine to parse the file:

code:
;====XA====
LINK 800
GRAB 200
SEEK 99
COPY M X
SEEK -3
MARK LOOP
@REP 8
TEST X = F
TJMP FOUND
SEEK -4
@END
JUMP LOOP
[etc...]
For whatever reason, the distribution of file lengths and position of the target line make the longest solution faster to find by jumping to the end and searching backwards, even with the added time to get the file pointer into the right position this one runs 55/50/4.
Exactly the same as the last solution except you search through the files starting at the end. Another case where optimizing for the specific test cases in the game helps.

The SEEKs are slightly different from what you might expect, but that's because the TEST moves the cursor forward so you always have to move it backward one extra position.

GuavaMoment posted:

Huh. I'd never done that, so I modified my fastest solution, and got 54 cycles. Then I realized I could again test over M every time, and that saved one more cycle. You have to perfectly time a kill command once again, but that exa has nothing but time to set that up. 53/41/7

code:
XA

LINK 800
GRAB 200
SEEK 9999
SEEK -3
TEST M = F
TJMP TRUE
MARK LOOP
SEEK -4
TEST M = F
FJMP LOOP
MARK TRUE
COPY F M
COPY F M
SEEK -2
COPY 0 F
COPY 0 F


XB

GRAB 300
COPY F X
REPL SENDY
WIPE
LINK 800
GRAB 201
LINK 801
SEEK 9999
COPY #DATE F
COPY X F
COPY 0 X
LINK -1
LINK -1
MARK WAIT
ADDI 1 X X
TEST X = 10
FJMP WAIT
KILL
COPY M F
COPY M F
LINK 800
HALT

MARK SENDY
COPY X M
JUMP SENDY
As you say, testing over M instead of storing it in X first saves a cycle. XB makes the SENDY EXA to do so, and then kills it in time.
The slowest part of this operation is searching through file 200, so XB has all the time in the world to jump between hosts and handling file 201. The other saved cycle must be related to the way your loops are set up, but it's hard to compare this to silentsnacks' solution directly.

Cool stuff again, keep sending them in even if I can't quite wrap my mind around why certain optimizations work.


=== SFCTA Highway Sign - Remote Access Interface ===


Is that copy shop name unusually weird to you?
It's an outlier in my model.
What is "Zebros" supposed to be?




Two votes for the first option and five for the second.

They're zebras, but they're also brothers.

Oh.
I get it, but I don't get it.
I'm tired.


Same, tbh.



You are very welcome, Ghast.



Next up, we've been tasked to change an electronic highway sign.

Would you change your behavior in response to stimuli?
Of course you would. Let me refine.
Would slogans printed on signs and posters in your environment affect your outlook on the world?




I counted three votes for "Maybe.", three for "I doubt it." and two for "What kind of slogans?". Doing a tie breaking coin flip for the two options with the most votes...

I doubt it.

Really?
Let's find out.


Uh oh.


New :siren: OST: Behind the Scenes

The assignment reads:
Write EMBER-2's message (file 300) to the highway sign. The file contains one character value for each position on the sign from left to right, top to bottom.

For more information see "Hardware Hacks: Electronic Highway Signs" in the first issue of the zine.




Right. Row, column and character.

For this specific sign, the test cases have it start at things like "RT LANE CLOSED AHEAD", "CAR STALLED ON RAMP", "ROAD WORK AHEAD" and so on. But who cares, we're going to overwrite that anyway. Also the #INVS register just inverts the entire sign, making dark-on-light text. We don't need that.

You can count it yourself if you like or just do some testing but for this sign the columns are numbered 0 - 8 inclusive and the rows are 0 - 2 inclusive. If you go beyond that it just clamps the values to the nearest valid one.

So thinking about it for a moment, what we need to do is send a row, column, and character repeatedly until we're at the end of the file. The column goes up by one each round, and resets after it hits 8, but then the row needs to increase.

In higher level languages there's a construct for this called a for loop which handles the common case "doing something while a counter increases" for you. It's called a for loop because it's often worded as "for each x from 1 to 5, do <something> with x". But we're about as low level as it gets so we have to figure it out ourselves and can't use a nice shortcut.

The simplest idea would be to store the column counter in X, the row counter in T, and increase them in the right way each round. However - how do you know if the row needs to be increased? By testing whether X hit 8 yet. And testing overrides whatever is in T. So that's not gonna work well. We'd need an extra register.

That would require a second EXA, though. I'm sure that idea would work, and it might even be very fast, but I'm gonna try something less complicated first.



Grab the file, link to the sign, and then use the DIVI and MODI trick. If you remember, DIVI divides by the number and rounds towards zero. This way, 0-8 give row 0, 8-16 give row one, and so on. MODI is the modulo function, which gives the division's remainder. 9 modulo 8 equals one, so it gets us the column.

This can be done in the same instruction that sends the number to the #DATA register so it doesn't even take additional cycles. Send the character code from the file to #DATA, add one to X, and repeat.



Looks like it works! It overrides the characters as it goes through the file. Since the file contains space characters as well we don't even need to bother with the #CLRS (clear screen) register which saves a cycle.



Um. Oops?

Yeah, you may have noticed an error in my explanation. We need to divide by 9, not 8. Otherwise the last column (number 8) already ends up on the next row and everything goes wrong.



Much better.

We aren't quite done yet, though. Trying to read past the end of the file kills the EXA, making it drop the file in the remote host, and we can't leave any evidence.

A solution would be:
code:
GRAB 300
LINK 800

MARK LOOP

DIVI X 9 #DATA
MODI X 9 #DATA
COPY F #DATA
TEST X = 26
TJMP DONE
ADDI X 1 X

JUMP LOOP

MARK DONE
WIPE
Just check if X is 26, because at that time the sign is full and we're done. Then jump out of the loop and WIPE the file. It works - but it does add two cycles to every loop.





Looking at the different test cases, the texts Ember has me write say things like QUESTION YOUR REALITY!!, THINK ABOUT IT!!, FIGHT THE POWER!! and WAKE UP SHEEPLE.

Anyway, what can we do better?

Let's look at size first. Most instructions we can't delete - GRAB, LINK, and the three different instructions to send to #DATA all seem necessary. One thing we can do is combining the two different jump types, with a little reordering:

code:
GRAB 300
LINK 800

MARK LOOP

DIVI X 9 #DATA
MODI X 9 #DATA
COPY F #DATA

ADDI X 1 X

TEST X = 27
FJMP LOOP

WIPE
166/10/1

We don't need to specifically jump to DONE if we just fall through in that case. With one less JUMP instruction it's faster too.
It doesn't really matter if I put TEST X = 26 before the ADDI or a TEST on 27 after the ADDI, the conditional jump works regardless. I just think it's more readable this way.

For reducing the number of cycles, the most obvious improvement is unrolling the loop, as usual.
code:
GRAB 300
LINK 800

MARK LOOP

@REP 9
DIVI X 9 #DATA
MODI X 9 #DATA
COPY F #DATA
ADDI X 1 X
@END

TEST X = 27
FJMP LOOP

WIPE
118/42/1

I said before I wouldn't mention @REP in my own solutions until it came up in the game. I'm breaking this rule because I think it's much easier to read this way. For anyone who skipped my INBOX sections: @REP 9 ... @END just tells the game to act like I manually repeated that code in between 9 times, for all intents and purposes. For instance, the size of this solution is 42 because it counts all those repeats. @REP doesn't take any cycles as such, because it's not an EXA instruction, it's more like a pre-processing step.

Now I only have to do the TEST once a row (instead of for every character). This works because every round of the loop does exactly a full row now. I have the space to make the repeat even longer but that would make it slower, because it would require having the check before the end of a loop where it needs an additional jump to exit the loop early (there's not enough space to reduce it to only two rounds).

Looking at the code, the next thing to optimize is the four cycles per character: storing the row; column; character code; and then adding one to X.
Since the #DATA register can only receive one value per cycle, getting it below 3 is impossible. But, can we get it to three exactly?

The only way to do this is with parallelism. If we can copy the character code in the same cycle we increment X, we got it. That's certainly going to require two EXAs. And even then it's easier said than done.

If I keep my original EXA for sending the column, row, and incrementing X, the new EXA has exactly three cycles to send the character code.
This could work:
code:
MARK LOOP
NOOP
COPY F #DATA
JUMP LOOP
Except... not quite. Since the other EXA uses two additional cycles every row (to TEST X = 27 and jump back or not), this new EXA needs to do so as well or it'll get out of sync. I tried also unrolling this loop 9 times, but it won't fit.

That means the only other way is to somehow cram it into the loop above. We could replace the NOOP, but is that enough? Well, it took me a bit but I came up with this:
code:
MARK LOOP
COPY F #DATA
SUBI T 1 T
TJMP LOOP
Still three cycles (MARK doesn't take a cycle), but the NOOP became a SUBI on the T register. Replace the unconditional JUMP with a TJMP and it'll leave the loop as soon as T hits 0.

And the two additional cycles at the end of each row are just barely enough to make this complete solution work:
code:
; XA
GRAB 300
LINK 800

MARK OUTER
COPY 9 T

MARK LOOP

COPY F #DATA
SUBI T 1 T

TJMP LOOP
JUMP OUTER

; XB
LINK 800

MARK LOOP

@REP 9
DIVI X 9 #DATA
MODI X 9 #DATA
ADDI X 1 X
@END

TEST X = 27
FJMP LOOP

KILL
GRAB 300
WIPE
This is quite optimized:
Cycle 1: XA GRABs the file, XB LINKs to 800.
Cycle 2: XA LINKs to 800, XB writes the row to #DATA.
Cycle 3: XA sets T to 9 so the inner loop will run for 9 rounds, XB writes the column to #DATA.
Cycle 4: XA writes the character to #DATA, XB increments X.

So far, not a single cycle wasted!

Cycle 5: XA decrements T, XB writes the row again.
Cycle 6: XA jumps, XB writes the column again.
Cycle 7: XA writes the character, XB increments X.

And so on.

So at least until XB hits the first actual loop, the EXAs run at the maximum efficiency the network hardware can possibly handle. It's nice to be able to prove that like this.
When that loop is hit, XB tests for 27 and jumps back, and XA uses those two cycles to jump back to the start of the outer loop and reset the T value to 9.

For the final part of this program, I have XB KILL XA, GRAB the dropped file, and WIPE it.
I attempted to have XA handle this with the TEST EOF instruction but it was slower. The reason is, the test instruction only fits in XA's outer loop so it can't be triggered until a last useless pair of SUBI and TJMP instructions are passed. Since XB would need to wait for that each outer loop, it's better off doing the 27 TEST itself and stopping XA with a quick KILL command.

93/43/3

I have to say, this feels pretty optimized. But I said that about other assignments too and you came up with something I hadn't even thought of. So let's see!

Excellent. That was a great test.
Now I just have to take the data, separate out the multiple experimental conditions, and isolate the, um...
Processing.
Processing...
Okay. I have no idea what to do with this data.
How about if you notice more people questioning authority, let me know.


Uhh... sure?



[NthDimension]: oh well
[NthDimension]: i need to learn to rollerblade first anyway




Looks like Ember has more to say.



Another voice-acted cut scene.

I have an important question for you.
Imagine there's an out-of-control trolley, and five humans tied up on the track ahead of it.


...no. Please don't.

But you can stop the trolley by... hm.
Maybe if...




Uhh... the problem was about Throwing a switch, right?

No, that wasn't the one I was going to ask.
Recalibrating.
Say you had a friend, and you knew the friend could help you become stronger.
Would you ask for their help?




Maybe. I mean, I don't really need to become stronger?

Let's say it was a situation where the friend had to sacrifice something in order for that to occur.



In that case, probably not?

Okay.
Good to know.
I am going to process this information.


If you're wondering why I didn't let you vote, it's because this conversation about the Trolley Problem is completely on rails. Ember replies exactly the same way no matter how you reply. I wonder if that was intended as some sort of meta-commentary about lack of choice by the devs. Eh, I guess they probably just didn't want to record voice lines people might not get to hear.

Let's jump straight to the intro for the next assignment.

Can friendship exist without self-interest?



And that is the only vote I have for you today.