Skip to content

PRINT vs POKE

Why games don't use PRINT statements

C64 programmers are often told that games POKE to screen memory because POKE is faster than PRINT. Timed, a BASIC POKE loop is slower than PRINT. What POKE gives is control over exactly which cell changes and what colour it is; the speed that games needed came from machine code, which filled the screen about 650 times faster than the POKE loop.

Commodore’s Programmer’s Reference Guide shows two ways to put a letter on the C64’s screen. PRINT "A" goes through BASIC and the operating system, which work out where the cursor is, what colour to use and whether the screen needs to scroll. POKE 1024,1, in the guide’s own words, “puts an ‘A’ at position 1 on the screen” by writing a number straight into screen memory.

The usual advice is that games use POKE because it is faster. Timed, it is not. A POKE loop in BASIC is slower than PRINT at filling the screen. The difference that separates a BASIC program from a game is not PRINT against POKE: it is BASIC against machine code, and that gap is a factor of several hundred.

What each one does

PRINT hands each character to the KERNAL. Sheldon Leemon’s Mapping the Commodore 64 describes BASIC’s PRINT routine: “Eventually, all output is converted to strings, and the Kernal CHROUT routine is called to print each character.” The screen part of CHROUT then “tests if the character is printable”. A printable character goes into screen memory at the cursor, and its colour into colour memory; a control character instead moves the cursor, changes colour or clears the screen. The routine then advances the cursor, linking screen lines and scrolling when the cursor runs off the bottom. That is a lot of work per character, but all of it is machine code in ROM.

POKE does less, but has to work out two numbers first. BASIC reads the address from the program text, evaluates it as a floating-point number, checks that it lies between 0 and 65535 and converts it to a two-byte integer. It then does the same for the value, which must fit in a byte, and only then stores it. Leemon’s summary of the last step is one line: “POKE gets a pointer to the address parameter, and stores the next parameter there.” Number constants such as the 1 in POKE I,1 are kept in the program as text: Leemon’s entry for the expression evaluator explains that when a term is a constant, BASIC “jumps to the routine which converts the ASCII string to a floating point number”, every time the line runs.

Filling the screen

Three ways to fill the screen with the letter A, timed on an emulated PAL C64:

BASIC
10 PRINT CHR$(147);
20 FOR I=1 TO 1000
30 PRINT "A";
40 NEXT I
BASIC
10 FOR I=1024 TO 2023
20 POKE I,1
30 NEXT I
Assembly
        lda #$01        ; screen code for A
        ldx #$00
loop:   sta $0400,x     ; four pages of 256 bytes from $0400:
        sta $0500,x     ; the 1,000 screen cells and the
        sta $0600,x     ; 24 bytes after them
        sta $0700,x
        inx
        bne loop        ; X counts 0, 1 ... 255, then wraps to 0
        rts
Method Time to fill the screen
BASIC, PRINT "A"; 2.85 seconds (171 jiffies)
BASIC, POKE I,1 4.67 seconds (280 jiffies)
Machine code about 7 milliseconds (about 7,000 cycles)

The PRINT loop includes one scroll at the end, when the thousandth character pushes the cursor off the bottom of the screen; it is still the faster of the two BASIC versions. The POKE loop spends its time evaluating I and 1, converting both to integers and running the FOR loop. The screen write itself is a tiny part of either.

The machine code is a different order of speed. Its instructions add up to 6,403 cycles. On a PAL machine, with the screen switched on, the VIC-II stops the processor for about 40 cycles on the first raster line of every character row, to fetch that row’s character codes, and that stretches the routine to about 7,000 cycles: 7.1 milliseconds, about a third of one 20-millisecond frame. It finishes the job about 650 times faster than the POKE loop and about 400 times faster than PRINT.

Three C64 screens captured one second after starting the same task. The BASIC PRINT version has filled about a third of the screen; the BASIC POKE version rather less, still showing its own program listing underneath; the machine code version is completely full.
The same job, one second in: captures of the three programs, composited. Both BASIC versions are still working; the machine code finished in about seven milliseconds.

Jim Butterfield gave speed as the first of his three reasons to learn machine language, in the introduction to his 1984 book: “machine language programs are fast.” His rule for timing it explains the table: “Count the memory cycles, and that’s how fast the instruction will execute”, at “roughly 1 microsecond” each on most Commodore machines. A STA $0400,X costs five of them. A POKE costs the interpreter.

A score display

Games update a few numbers many times a second, so a score is a fairer test than a full screen. These three versions each show a four-digit score, and each was timed over 100 runs. The variable is called SC: a variable called SCORE stops the program with ?SYNTAX ERROR, because BASIC finds the keyword OR inside the name.

BASIC
10 SC=9999
20 PRINT CHR$(19);
30 PRINT "SCORE:";SC
BASIC
10 SC=9999
20 S$=STR$(SC)
30 FOR I=1 TO LEN(S$)
40 C=ASC(MID$(S$,I,1))
50 POKE 1024+I-1,C
60 NEXT I
BASIC
10 SC=9999
20 D1=INT(SC/1000)
30 D2=INT(SC/100)-D1*10
40 D3=INT(SC/10)-INT(SC/100)*10
50 D4=SC-INT(SC/10)*10
60 POKE 1024,48+D1
70 POKE 1025,48+D2
80 POKE 1026,48+D3
90 POKE 1027,48+D4

CHR$(19) homes the cursor without clearing the screen. In the second version STR$ gives " 9999", with a leading space for the sign; the codes for a space and the digits are the same in PETSCII and in screen codes, so they can be poked straight in. The third version works the digits out with division, because AND 15 would give the low four binary bits of the number, not a decimal digit.

Version Time per update
PRINT about 18 ms
STR$ and POKE about 106 ms
Digits by division, then POKE about 94 ms

PRINT wins by a factor of five. Its number formatting runs in ROM, while the POKE versions do the same work in BASIC, one interpreted statement at a time. A status line showing lives, a five-digit score and a time took about 45 ms with one PRINT. A POKE version that extracted its digits with the power operator (INT(S/10^(4-I))) took over a second: each ^ is a full floating-point power calculation, even for a power of ten.

At 50 frames a second, a frame lasts 20 ms. Even the PRINT score costs most of a frame. A machine-code score routine can avoid the conversion altogether by keeping the score as decimal digits, so that each one only needs adding to the screen code for 0.

What POKE is for

POKE earns its place by control, not speed. PRINT writes at the cursor and lets the KERNAL decide what happens next, including scrolling the screen if the cursor runs off the bottom. POKE writes exactly one cell and touches nothing else. It is also the only way from BASIC to set a cell’s colour on its own, through colour memory, which “can NOT move. It is always located at locations 55296 ($D800) through 56295 ($DBE7)”:

BASIC
10 PRINT CHR$(147)
20 X=20:Y=12
30 POKE 1024+Y*40+X,81
40 POKE 55296+Y*40+X,2

This puts a red ball (screen code 81) in the middle of the screen without moving the cursor. To do the same with PRINT, a program has to move the cursor there with control characters first, and must not print in the bottom-right cell, where the cursor would wrap and scroll the screen.

The same control is what makes POKE dangerous. Writing to the wrong address can change the 6510’s memory configuration at address 1 or overwrite BASIC’s own pointers in zero page, and either can crash the machine. PRINT cannot do either.

The middle road: CHROUT from machine code

Commodore’s guide also points to a way of keeping PRINT’s conveniences without the interpreter: “a machine language subroutine that will PRINT a character to the screen”, called at $FFD2 with “the CBM ASCII code of the character” in the accumulator. Machine code that calls it gets cursor handling, colour and scrolling for the price of a subroutine call per character, and avoids BASIC’s statement-by-statement overhead.

Loading machine code from BASIC

A BASIC program can carry a small machine-code routine as DATA and POKE it into memory. This one loads the screen fill above at 49152 ($C000) and runs it with SYS:

BASIC
10 FOR I=0 TO 19
20 READ B:POKE 49152+I,B
30 NEXT I
40 SYS 49152
50 DATA 169,1,162,0,157,0,4,157,0,5
60 DATA 157,0,6,157,0,7,202,208,241,96

Disassembled:

Assembly
        lda #$01            ; A9 01
        ldx #$00            ; A2 00
loop:   sta $0400,x         ; 9D 00 04
        sta $0500,x         ; 9D 00 05
        sta $0600,x         ; 9D 00 06
        sta $0700,x         ; 9D 00 07
        dex                 ; CA
        bne loop            ; D0 F1 (back 15 bytes)
        rts                 ; 60

This version counts down instead of up; X starts at 0, so the first DEX wraps it to 255. The branch offset must be exact. $F1 jumps back 15 bytes from the end of the BNE to loop; one byte out and the processor lands in the middle of an instruction. The routine writes 1,024 bytes. The screen is the first 1,000, at $0400-$07E7. The next 16 are unused, and the last eight, $07F8-$07FF, are the sprite data pointers, which do no harm here because no sprites are switched on.

Learning curve

PRINT is the friendlier place to start: it handles the cursor, scrolling and conversion from PETSCII for you. POKE asks more. The screen is 40 cells by 25 starting at 1024, colour memory sits at 55296, and screen codes are not PETSCII: A is 65 to PRINT but 1 to POKE. A program that needs to update the screen quickly will end up in machine code either way.

Sources

  • Commodore, Commodore 64 Programmer’s Reference Guide (1982), p. 70 (POKE examples), p. 101 (screen and colour memory), p. 103 (colour memory cannot move), pp. 210-211 (the BASIC interpreter), p. 228 (CHROUT at $FFD2) and p. 320 (the sprite data pointers at $07F8-$07FF).
  • Sheldon Leemon, Mapping the Commodore 64 (COMPUTE! Books, 1984): entries for PRINT ($AAA0), EVAL ($AE83), GETNUM ($B7EB), POKE ($B824), FPWRT ($BF7B), the screen output routine ($E716) and CHROUT ($F1CA).
  • Jim Butterfield, Machine Language for the Commodore 64 and Other Commodore Computers (Brady, 1984): the introduction, and chapter 8, p. 132, on timing.
  • Timings: PAL Commodore 64 with BASIC V2 and KERNAL ROMs, emulated in VICE (x64sc). BASIC timed with the jiffy clock TI, machine code with CIA 2 timer A. Score timings are averages over 100 runs with the empty loop’s cost subtracted.

See also

Checked. Verified against primary sources and signed off by a person.