Simple8

Inline Assembly and Memory

Until now, libraries have been the link to the hardware. This chapter opens the hood: how the machine's memory is organized, how you control the hardware by writing to specific addresses, and how to slip assembly into a Simple8 program when you need more speed.

The machine's memory

In chapter 6, we saw that memory is a sequence of numbered one-byte cells: each cell has an address. On an 8-bit machine, addresses go from $0000 to $FFFF, or 65,536 cells.

These cells are not all of the same kind. Depending on the address, you find working memory (RAM), program memory (ROM), or… the hardware itself. The layout of these areas is called the memory map. Here are the maps of the 8-bit machines Simple8 can program, as Simple8 organizes them:

Commodore 64 · 6510
$0000-$00FFzero page: fast access, fast variables
$0801-$5FFFthe resident program
$6000-$9FFFthe loaded context
$A000-$BFFFvariables
$C000-$C3E7the text screen: one byte per cell
$D000-$DFFFhardware: video ($D000), sound ($D400), screen colors ($D800)
PC Engine · HuC6280
$0000-$1FFFhardware: video processor ($0000), colors ($0400), sound ($0800)
$2000-$3FFFRAM: zero page, stack, variables
$4000-$DFFFcartridge banks: resources, contexts
$E000-$FFFFthe resident program
NES · 2A03
$0000-$07FFRAM, 2 KB: zero page, stack, sprites ($0200), variables
$2000-$2007the video processor (PPU) registers
$4000-$4017sound, and the joypads ($4016)
$6000-$7FFFcartridge RAM: extra variables
$8000-$BFFFthe loaded context
$C000-$FFFFthe resident program
Game Boy · SM83
$0000-$3FFFcartridge bank 0: the resident program
$4000-$7FFFthe bank of the loaded context
$8000-$9FFFvideo memory: tiles, and screen maps ($9800)
$C000-$DFFFRAM: variables, stack
$FF00-$FF7Fhardware registers
$FF80-$FFFEhigh RAM: fast variables
Amstrad CPC · Z80
$0040-$3DFFvariables
$3E00-$3FFFthe stack
$4100-$6FFFthe resident program
$7000-$9CFFthe loaded context
$A600-$BFFFthe machine's system, which reads the disk
$C000-$FFFFthe screen: 16 KB of pixels

From one machine to another, you find the same areas: RAM for variables, the resident program, room for the loaded context, and the hardware. Only their addresses change. Two differences are worth remembering:

These maps are simplified: each machine's documentation will give the details.

Hardware is controlled through memory

On these machines, you do not talk to the hardware through functions: you write numbers to certain addresses. These special cells are called registers. On C64, cell $D020 is not ordinary memory: the number you write there becomes the color of the screen border.

That is exactly what vic.setBorderColor, seen in chapter 2, does. Here is its code, in the vic.base library:

void setBorderColor(byte color) {
    _asm {
        LDA _params;        // the color parameter
        STA $D020;          // writes to the border register
    }
}

Libraries do nothing more than this: they give readable names to the hardware registers.

The Amstrad CPC case. The Z80 has two separate spaces: memory, and input/output ports, reserved for hardware. The CPC chips (video, colors, sound) sit on ports, which you reach with the Z80's IN and OUT instructions, in an _asm block or through its libraries. readMem and writeMem only reach memory.

Reading and writing memory: readMem and writeMem

Without leaving Simple8, two functions read and write a byte at any address:

writeMem(0xD020, 2);                    // C64: red border
ubyte b = readMem(0xD020) & 0x0F;       // read its color back: 2

Some registers need several writes. On PC Engine, to change a color, you first give its number to the color chip (addresses $0402 and $0403), then its value ($0404 and $0405):

uint bleu = RGB(0, 0, 7);
writeMem(0x0402, 0);                    // color number 0...
writeMem(0x0403, 0);
writeMem(0x0404, (ubyte)(bleu & 0xFF)); // ... gets the low byte
writeMem(0x0405, (ubyte)(bleu >> 8));   // ... then the high byte: the background turns blue

Use with care. writeMem writes wherever you tell it to, without checking: a wrong address can overwrite a variable or the program, or put the hardware in a bad state. Use it for a specific register, knowing the memory map.

Simple8 versus assembly

Simple8 frees the programmer from machine language: you write loops, variables and functions, and the compiler picks the instructions. Its optimizers then rework the result: they put counters in processor registers, avoid repeated calculations, and combine instructions.

But 8-bit machines are full of constraints: few registers, no multiplication, calculations done 8 bits at a time. Simple8 does its best with them. Depending on the case, its code is as fast as assembly, faster than simply written assembly, or much slower than assembly written by someone who knows the machine inside out. Here are examples of each case, all measured on C64.

What works well

Simple cases. Writing a color to the border register, in Simple8 or in assembly, produces exactly the same instructions:

writeMem(0xD020, 2); in Simple8

LDA #2
STA $D020

_asm { LDA #2; STA $D020; } in assembly

LDA #2
STA $D020

A well-written loop. Filling the 1,000 cells of the screen with a ubyte counter and fast pointers:

fast ubyte *p0;                     // four pointers in zero page,
fast ubyte *p1;                     // one per quarter of the screen
fast ubyte *p2;
fast ubyte *p3;

.p0 = 0xC000;
.p1 = 0xC0FA;
.p2 = 0xC1F4;
.p3 = 0xC2EE;
for (ubyte i = 0; i < 250; i++) {
    .p0[i] = 0x66;
    .p1[i] = 0x66;
    .p2[i] = 0x66;
    .p3[i] = 0x66;
}

The optimizer turns it into a loop an assembly programmer would be happy with: the counter i becomes register Y, the value is loaded only once per pass, and the four writes go straight through the pointers:

boucle:
    LDA #102
    STA (Main_p0),Y
    STA (Main_p1),Y
    STA (Main_p2),Y
    STA (Main_p3),Y
    INY
    CPY #250
    BCC boucle
Filling the screenCyclesTime
simply written assembly: one pointer moved cell by cell25,66326 ms
Simple8, four fast pointers8,3098.4 ms
hand-optimized assembly: four strips, indexed addressing6,4126.5 ms

Here, Simple8 is three times faster than assembly written without tricks, and 1.3 times slower than the best assembly, which uses the indexed addressing described below.

What works less well

The same loop, written without thinking about the machine, is much slower:

Filling the screenCyclesCompared to the best assembly
ubyte counter, writeMem(0xC000 + i, …)28,7674.5 times slower
uint counter, writeMem(0xC000 + i, …)49,3697.7 times slower

Nothing is wrong here: the compiler simply recomputes the 16-bit address on every pass, because that is what the program asks for. Other situations are expensive on an 8-bit machine, whatever the compiler:

The algorithm first

The choice of language is often not the most important thing: the way you compute matters more. Example: the address of line y of the screen, $C000 + y × 40.

// with a multiplication: the 6502 cannot multiply, so it must be computed
.adresse = 0xC000 + y * 40;

// with a table: the 25 results, computed once and for all
const uint LIGNES[] = { 0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
                        520, 560, 600, 640, 680, 720, 760, 800, 840, 880, 920, 960 };
.adresse = 0xC000 + .LIGNES[y];
Computing y × 40Cycles per calculation
with a multiplication, y * 4082
with a table, .LIGNES[y]10
with shifts, ((uint)y << 5) + ((uint)y << 3)112

Knowing the machine

Simple8 spares you from writing machine language, not from understanding the machine. Knowing what it does fast (8 bits, zero page, indexed addressing, tables) and what it does poorly (16 and 32 bits, multiplications) helps you write efficient Simple8. A few rules:

When should you write assembly?

For most of a program, Simple8 is enough, and its flexibility saves far more time than it costs. Assembly becomes useful in a few cases:

That is what the libraries in libs8/ do: they keep assembly where it matters, and the program stays in Simple8.

First steps in 6502 assembly

The C64 has a 6502 processor (its variant, the 6510); the PC Engine has a HuC6280, which is an extended version of it. Their assembly language is the same at its core.

The processor registers

The processor does not compute directly in memory: it works in a few internal cells, its registers, of 8 bits each.

A few instructions

InstructionEffectExample
LDA, LDX, LDYload a value into A, X or YLDA #2: A is 2
STA, STX, STYstore A, X or Y in memorySTA $D020: the border takes color A
INX, DEXadd 1 to X or subtract 1 from X
CLC then ADCadd to ACLC ADC #3: A = A + 3
ANDbitwise “and” with AAND #$0F: keep the low 4 bits
TXAcopy X into A
CPXcompare X with a valueCPX #4
BNEbranch if the previous result is not zeroBNE boucle

Value or address?

The _asm block

Inside a function, an _asm { … } block contains processor instructions, inserted as is into the program:

void bordureRouge() {
    _asm {
        LDA #2;             // A = 2, red
        STA $D020;          // the border
    }
}
_asm {
    LDX #0;
    LDA #0;
boucle:                     // a label
    CLC;
    ADC #3;                 // A = A + 3
    INX;
    CPX #4;
    BNE boucle;             // repeat while X is not 4: A is 12
}

Variables in an _asm block

An _asm block can read and write the program's variables. Each kind of variable is written its own way:

VariableIn _asmExample
local, or function parameter\nomLDA \code;
of the unitits label: Unit_nomLDA Main_score;
of another unitUnit.nomLDA Joueur.x;
high byte of a uint+1LDA Main_grand+1;
low byte, high byte of its address#<, #>LDA #<Main_score;
unit {
    ubyte score = 5;
    ubyte a;
    ubyte b;

    main() {
        ubyte locale = 9;
        _asm {
            LDA Main_score;         // the score variable of the Main unit...
            STA Main_a;             // ... copied into a: 5
            LDA \locale;            // the local variable...
            STA Main_b;             // ... copied into b: 9
        }
    }
}

In a unit, the name alone is not enough. LDA score; is left as is, and the assembler finds nothing: Undefined symbol: score. Write the full label: the unit name, an underscore, then the variable name: Main_score. These are also the names you see in the generated assembly and in the .sym file.

One assembly language per processor

Each processor has its own instruction set, and Simple8 checks each _asm block against the processor of the chosen machine:

The compiler's work cells. For its calculations, Simple8 uses zero page cells named t0 to t7. An _asm block must not modify them, unless you know exactly what you are doing.

An example: a checkerboard screen

This program fills the whole C64 screen with a checkerboard character, then gives each cell a color, all with two assembly functions. Simple8 sets up the screen and writes the title.

// Inline assembly and memory: a checkerboard screen (Commodore 64)
architecture commodore.c64;

addlib vic.base;
addlib vic.screentext;

unit {
    const ubyte DAMIER = 0x66;          // the screen code of the checkerboard character

    main() {
        vic.initScreen();
        vic.setBorderColor(vic.BLACK);
        vic.setBackgroundColor(vic.BLACK);

        remplirEcran(.DAMIER);
        colorierEcran();

        vic.setFontColor(vic.WHITE);
        vic.setCursor(14, 12);
        vic.print(" ASSEMBLEUR ");
    }

    // Writes the same character to the 1,000 screen cells ($C000 to $C3E7)
    void remplirEcran(ubyte code) {
        _asm {
            LDA \code;                  // the code parameter
            LDX #0;
        boucle:
            STA $C000,X;                // 4 strips of 256 cells, overlapping
            STA $C100,X;                // a little to end exactly at $C3E7
            STA $C200,X;
            STA $C2E8,X;
            INX;
            BNE boucle;                 // 256 passes: X wraps back to 0
        }
    }

    // Gives each cell a color from 0 to 15 ($D800, the color memory)
    void colorierEcran() {
        _asm {
            LDX #0;
        boucle:
            TXA;
            AND #$0F;                   // the color: X modulo 16
            STA $D800,X;
            STA $D900,X;
            STA $DA00,X;
            STA $DAE8,X;
            INX;
            BNE boucle;
        }
    }
}
Commodore 64 screen filled with a checkerboard in 16 colors, with ASSEMBLEUR in the center
The screen filled and colored in assembly.
The PC Engine version

On PC Engine, the screen is not in the processor's memory, but in video memory, reached through the registers of the video processor (VDC). The HuC6280 has instructions made for this: ST0 selects a VDC register, ST1 and ST2 write the low byte and the high byte to it.

// Inline assembly and memory: a checkerboard screen (PC Engine)
architecture nec.pce;

addlib sys.resource;
addlib vce.base;
addlib vdc.base;
addlib vdc.screentext;

unit {
    const int font = addResource("font.bin");
    const uint COULEURS[] = {
        RGB(7, 0, 0), RGB(7, 4, 0), RGB(7, 7, 0), RGB(0, 6, 0),
        RGB(0, 6, 6), RGB(0, 2, 7), RGB(5, 0, 7), RGB(7, 7, 7)
    };

    main() {
        int source;
        int length;

        vdc.setBATSize(vdc.BAT_32x32);
        vdc.initScreen();
        resource.mapToMPR2(.font);
        source = 0x4000 + resource.getOffset(.font);
        length = resource.getLength(.font);
        vdc.writeToVRAM(source, 0x1000, length);
        vce.setPalette(0, 0, RGB(0, 0, 0));
        for (ubyte p = 0; p < 8; p++) {
            vce.setPalette(p, 1, .COULEURS[p]);     // palettes 0 to 7: 8 colors
        }

        remplirEcran();

        vdc.setFontColor(7);
        vdc.setCursor(10, 13);
        vdc.print(" ASSEMBLEUR ");
    }

    // Fills the 1,024 cells of the screen map (BAT, in video memory at address 0)
    // with the '+' character, in palettes 0 to 7 in turn
    void remplirEcran() {
        _asm {
            ST0 #0;                     // VDC register 0: the write address...
            ST1 #0;
            ST2 #0;                     // ... 0, the start of the map
            ST0 #2;                     // register 2: the data to write
            LDY #4;                     // 4 times 256 cells
            LDX #0;
        boucle:
            ST1 #$2B;                   // low byte of the cell: the '+' tile
            TXA;
            AND #$07;                   // the palette: X modulo 8...
            ASL A;
            ASL A;
            ASL A;
            ASL A;                      // ... in the high 4 bits
            ORA #$01;                   // + the high part of the tile number ($12B)
            STA $0003;                  // high byte: the cell is written
            INX;
            BNE boucle;
            DEY;
            BNE boucle;
        }
    }
}
  • Each cell of the screen map is a 16-bit word: the tile number (12 bits) and the palette number (4 bits).
  • VDC register 0 receives the address to write to, register 2 the data. After each word is written, the address moves forward by itself.
  • Two counters, X and Y, make 4 × 256 = 1,024 passes: the 32 × 32 cells.
PC Engine screen filled with + signs in 8 colors, with ASSEMBLEUR in the center
The screen map filled in assembly, on PC Engine.

A special case: the Mega Drive and its two processors

The Mega Drive has two processors, working at the same time:

Their machine languages have nothing in common: MOVE.W #5, D0 for the 68000, LD A, 5 for the Z80. Yet a single Simple8 program contains code for both. How does the compiler know which assembly language to use for an _asm block?

The processor is chosen per unit

The answer is in main.s8p: the units of the main program run on the 68000; the ones you declare in a coprocessor block run on the Z80. So each _asm block is written in the language of its unit's processor.

main.s8p

// Two processors, two assembly languages (Mega Drive)
architecture sega.megadrive;

coprocessor Son : zilog.z80 {
    addunit("Son.s8u");             // this file is Z80 code
}

unit {
    uint compteur;

    main() {
        _asm {
            MOVE.W #5, D0;          // 68000 code: the main unit runs on the 68000
            ADD.W #2, D0;
            MOVE.W D0, Main_compteur;
        }
        Son.demande = 1;            // a shared variable of the Z80
        while (1) {
        }
    }
}

Son.s8u

// Son.s8u: the Z80 program
unit Son {
    shared {
        ubyte demande;              // written by the 68000
        ubyte faites;               // read by the 68000
    }

    main() {
        while (1) {
            if (.demande != 0) {
                _asm {
                    LD A, (Son_faites);     // Z80 code: this unit runs on the Z80
                    INC A;
                    LD (Son_faites), A;
                }
                .demande = 0;
            }
        }
    }
}

The compiler checks each block with the right assembler. A Z80 instruction in the main unit, or a 68000 instruction in Son, is rejected:

Unknown mnemonic 'LD' for architecture Motorola 68000 (Mega Drive)
coprocessor Son: Unknown mnemonic 'MOVE.W' for architecture Zilog Z80 (Mega Drive)

Which processor for which task?

68000Z80
Speed7.6 MHz, 16- and 32-bit calculations3.58 MHz, 8-bit calculations
Memorythe cartridge and 64 KB of RAM8 KB for everything: code, data, stack
For…the game: rules, graphics, calculationssound: music and sound effects, which play without taking time from the game
Librariesvdp, pad…sound.psg, sound.player, sound.mixer…

So the choice follows how tasks are divided, more than speed: the Z80 is slower, but it works in addition to the 68000. Giving it the music frees the 68000 for the game. The Mega Drive documentation details how to start the Z80 and its libraries.

Key points

Your turn

  1. With writeMem, cycle the C64 border through its 16 colors in a loop.
    Show solutionHide solution

    The border register is at address $D020: a loop from 0 to 15 writes each color number there in turn with writeMem. But a loop alone is not enough: it would run so fast that the eye would see nothing. You need to wait a little between two colors, and chapter 8 gave us a real clock: the irq interrupt, 50 times per second. Here we wait 25 frames, half a second, per color.

    // The border cycles through the 16 colors, with writeMem (Commodore 64)
    architecture commodore.c64;
    
    addlib event.irq;
    
    unit {
        ubyte images;                       // counted by the interrupt, 50 times per second
    
        main() {
            while (1) {
                for (ubyte c = 0; c < 16; c++) {
                    writeMem(0xD020, c);    // the border register
                    .images = 0;
                    while (.images < 25) {  // half a second per color
                    }
                }
            }
        }
    
        interrupt -> irq {
            .images++;
        }
    }

    Run in the emulator, the program does show the border in white, then red, cyan, purple… one color every half second, then the cycle starts over.

    And without the wait? The result is surprising, and instructive: the loop changes the color several times while the picture is being drawn. The border then fills with thin stripes, each color lasting only a few lines of the screen.

    architecture commodore.c64;
    unit {
        main() {
            while (1) {
                for (ubyte c = 0; c < 16; c++) {
                    writeMem(0xD020, c);
                }
            }
        }
    }
    The C64 border covered with thin stripes of every color
    Without waiting: the border changes color while the picture is being drawn.

    This is the principle behind the “color bar” effects of demos, which change a color at a precise line of the screen: chapter 12 will come back to it.

  2. Modify colorierEcran to use only colors 8 to 15.
    Show solutionHide solution

    Colors 8 to 15 are the ones whose bit 3 (the value 8) is 1. So you keep the low 3 bits of X with AND #$07, which gives 0 to 7, then force bit 3 with ORA #$08, the bitwise “or”: you get 8 to 15. A CLC followed by ADC #8 would give the same result, but ORA does in one instruction what the addition does in two.

        // Gives each cell a color from 8 to 15 ($D800, the color memory)
        void colorierEcran() {
            _asm {
                LDX #0;
            boucle:
                TXA;
                AND #$07;                   // X modulo 8: from 0 to 7...
                ORA #$08;                   // ... plus 8: from 8 to 15
                STA $D800,X;
                STA $D900,X;
                STA $DA00,X;
                STA $DAE8,X;
                INX;
                BNE boucle;
            }
        }
    The C64 checkerboard in columns of paler colors: orange, brown, pink, gray, light green, light blue
    The checkerboard in colors 8 to 15: the C64's light shades.

    Black and white are gone, and the pattern looks different: the colors now repeat every 8 cells. A screen line has 40 cells, a multiple of 8: so each line starts with the same color, and the columns form straight stripes. In the chapter's example, the cycle of 16 colors shifted by 8 from one line to the next (40 = 2 × 16 + 8), hence its interlaced look.

  3. Write an assembly function viderEcran() (clear screen) that writes spaces ($20) to the whole screen, and call it instead of remplirEcran: only the title remains.
    Show solutionHide solution

    viderEcran is a copy of remplirEcran, with one difference: the character is no longer a parameter, but the fixed value #$20, the screen code for a space. The # is essential: without it, LDA $20 would read memory cell $20, in zero page, instead of loading the value 32.

        main() {
            vic.initScreen();
            vic.setBorderColor(vic.BLACK);
            vic.setBackgroundColor(vic.BLACK);
    
            viderEcran();                   // instead of remplirEcran(.DAMIER)
            colorierEcran();
    
            vic.setFontColor(vic.WHITE);
            vic.setCursor(14, 12);
            vic.print(" ASSEMBLEUR ");
        }
        // Writes a space ($20) to the 1,000 screen cells ($C000 to $C3E7)
        void viderEcran() {
            _asm {
                LDA #$20;                   // the screen code for a space
                LDX #0;
            boucle:
                STA $C000,X;                // the same 4 strips as remplirEcran
                STA $C100,X;
                STA $C200,X;
                STA $C2E8,X;
                INX;
                BNE boucle;
            }
        }

    The screen stays black, with only “ASSEMBLEUR” (assembler) in the middle. colorierEcran still runs, but a space has no pixel lit: its color cannot be seen. You could remove this call without changing anything on screen. Notice also that remplirEcran, which is no longer called, is not copied into the program: the asm/main.asm file says so with the line [UNUSED] Function Main_remplirEcran - skipped.

  4. Open asm/main.asm and compare the code of remplirEcran with that of a Simple8 loop that does the same thing with writeMem.
    Show solutionHide solution

    To compare, add to the program a Simple8 version of remplirEcran, the most natural one: a counter from 0 to 999 and one writeMem per cell. Call it instead of the assembly version.

        // The same thing in Simple8, with writeMem
        void remplirEcranS8(ubyte code) {
            for (uint i = 0; i < 1000; i++) {
                writeMem(0xC000 + i, code);
            }
        }

    The assembly version, in the asm/main.asm of the chapter's program, is copied as is: 6 instructions in the loop, which runs 256 times.

    ; === Function: Main_remplirEcran ===
    Main_remplirEcran:
    ; <asm>
    	LDA _params+0
    	LDX #0
    Main_remplirEcran_boucle_1:
    	STA $C000,X
    	STA $C100,X
    	STA $C200,X
    	STA $C2E8,X
    	INX
    	BNE Main_remplirEcran_boucle_1
    ; </asm>
    	RTS
    ; === End: Main_remplirEcran ===

    The Simple8 version produces this (the compiler also copied it directly into main(), at the call site: it is the same loop):

    ; === Function: Main_remplirEcranS8 ===
    Main_remplirEcranS8:
    ; [Main:L40] Main_remplirEcranS8__FOR_1_i = #0
    	LDA #<0
    	STA Main_remplirEcranS8__FOR_1_i
    	STA Main_remplirEcranS8__FOR_1_i+1
    ; === Loop: _FOR_1 ===
    _FOR_1_start:
    ; Loop condition: Main_remplirEcranS8__FOR_1_i < #1000
    	LDA Main_remplirEcranS8__FOR_1_i+1
    	CMP #>1000
    	BCC _FOR_1_cond_ok
    	BEQ _FOR_1_cond_ok_skip1
    	JMP _FOR_1_end
    _FOR_1_cond_ok_skip1:
    	LDA Main_remplirEcranS8__FOR_1_i
    	CMP #<1000
    	BCC _FOR_1_cond_ok
    	JMP _FOR_1_end
    _FOR_1_cond_ok:
    ; [Main:L41] t0 = #49152 + Main_remplirEcranS8__FOR_1_i
    	LDA #<49152
    	CLC
    	ADC Main_remplirEcranS8__FOR_1_i
    	STA t0
    	LDA #>49152
    	ADC Main_remplirEcranS8__FOR_1_i+1
    	STA t0+1
    ; [L41] writeMem(t0, Main_remplirEcranS8_code)
    	LDA _params+0
    	LDY #0
    	STA (t0),Y
    ; Loop increment: Main_remplirEcranS8__FOR_1_i++
    	INC Main_remplirEcranS8__FOR_1_i
    	BNE _FOR_1_incr_skip
    	INC Main_remplirEcranS8__FOR_1_i+1
    _FOR_1_incr_skip:
    ; Loop end: _FOR_1
    	JMP _FOR_1_start
    _FOR_1_end:
    	RTS
    ; === End: Main_remplirEcranS8 ===

    Nothing is wrong here, but each pass does much more work:

    • i is a uint: it lives in memory, on two bytes, not in register X. Comparing it with 1000 takes two comparisons and several branches, and incrementing it takes two INC;
    • the address 0xC000 + i is recomputed on every pass: a 16-bit addition, stored in the work cell t0;
    • the write goes through this pointer, STA (t0),Y, whereas the assembly uses indexed addressing, STA $C000,X, which does the address calculation for free;
    • the loop makes 1,000 passes, versus 256 for the assembly, which writes 4 cells per pass.

    Measured on the C64 (subtracting the time of a program that only does vic.initScreen()):

    Filling the screenCyclesTime
    remplirEcran, in assembly6,4236.5 ms
    remplirEcranS8, writeMem and a uint counter51,37552 ms

    The Simple8 version is 8 times slower: about 51 cycles per cell, versus 6.4. This is not inevitable: the chapter shows that with a ubyte counter and four fast pointers, Simple8 gets down to 8,309 cycles. It is the way you write the loop, more than the language, that makes the difference.