Weak Performance of POWER C

When I wrote my first version of the 2D Game of Life in POWER C (2D Game of Life for the Commodore 64 – Jan-Arendt Klingel), I was very surprised of the bad performance of my code. I measured roughly 4.6 seconds per loop on a Commodore 64 with a CPU running at 2 MHz. Each do..while loop is calculating and printing a new generation of cells.

I modified the code to achieve more performance, but now I was even more surprised when this completely failed. With a bit above 5 seconds, my new code took even more time to go from generation to generation.

2dlife v1, loop-to-loop2dlife v2, loop-to-loop
4.6 seconds5.1 seconds

The difference in speed becomes apparent when running the new version versus the old version in two emulated VICE windows:

After a short while, the old code (right) overtakes the new code (left).

My first version used two char arrays with the dimension 24×40, the second two int arrays of the same dimension. I used the int array to calulate the active neighbor cells more easily. The problem is that a char takes up only one byte in memory whereas an integer takes up 2 bytes. In total the int array has 1920 (24x40x2) bytes, the char array has only 960 bytes. Going through all cells in two nested for loops and setting all cells to zero should therefore take longer with the int array than with the char array. And indeed:

char array[24][40] = 0int array[24][40] = 0
0.93 seconds0.97 seconds

The measurements were taken using the Jiffy clock before entering the nested for loop and after. This can be done using the following code:

#include <poke.h>
#define MIN 0xa1 /* Jiffy minutes   */
#define SEC 0xa2 /* Jiffy seconds   */
...
unsigned int sec, secold;
secold = 0;
...
sec = peek(char,MIN)*256+peek(char,SEC);
printf("Ticks: %u, Sec: %2.2f", sec-secold, (sec-secold)/60);
secold = sec;
...

So far I had not found the reason for the code to perform so slow.

Next, I tested going through the arrays and setting random unsigned int values between 0 and 1 in a 5×5 matrix in the middle of the array. Both iterations took 0.2 seconds, so there was no difference between the char and the int array. In other words, before the program enters the do..while loop, code execution is quite fast. The culprit must be inside the do..while loop.

The first thing that happens inside the do..while loop is counting the alive neighbor cells to the current cell:

2dlife v1, counting neighbors2dlife v2, counting neighbors
/* Count neighbors */
aliveN = 0;
if(screenX[x][y-1] == ALIVE) /* North */
aliveN++;
if(screenX[x+1][y-1] == ALIVE) /* Northeast */
aliveN++;
if(screenX[x+1][y] == ALIVE) /* East */
aliveN++;
if(screenX[x+1][y+1] == ALIVE) /*Southeast */
aliveN++;
if(screenX[x][y+1] == ALIVE) /* South */
aliveN++;
if(screenX[x-1][y+1] == ALIVE) /* Southwest */
aliveN++;
if(screenX[x-1][y] == ALIVE) /* West */
aliveN++;
if(screenX[x-1][y-1] == ALIVE) /* Northwest */
aliveN++
/* Count neighbors */
aliveN = sX[row][col-1]
+ sX[row+1][col-1]
+ sX[row+1][col]
+ sX[row+1][col+1]
+ sX[row][col+1]
+ sX[row-1][col+1]
+ sX[row-1][col]
+ sX[row-1][col-1];

The new version is much simpler and should perform faster.

Next is applying the rule book to the future generation cell:

2dlife v1, applying rule book2dlife v2, applying rule book
if(screenX[x][y] == DEAD && aliveN == 0)
;
else {
/* Apply rule book */
if(screenX[x][y] == ALIVE && aliveN < 2)
screenY[x][y] = DEAD;
else if(screenX[x][y] == ALIVE && (aliveN == 2 || aliveN == 3))
screenY[x][y] = ALIVE;
else if(screenX[x][y] == ALIVE && aliveN > 3)
screenY[x][y] = DEAD;
else if(screenX[x][y] == DEAD && aliveN == 3)
screenY[x][y] = ALIVE;
else
screenY[x][y] = DEAD;
}
/* Set new generation */
if(sX[row][col] == 0 && aliveN == 0)
;
else {
/* Apply rule book */
if(sX[row][col] == 1) {
switch(aliveN) {
case 0:
sY[row][col] = 0;
break;
case 1:
sY[row][col] = 0;
break;
case 2:
sY[row][col] = 1;
break;
case 3:
sY[row][col] = 1;
break;
default:
sY[row][col] = 0;
break;
}
}
else {
switch(aliveN) {
case 3:
sY[row][col] = 1;
break;
default:
sY[row][col] = 0;
break;
}
}
}

In version 2 of the code I separated the rule book into two pieces: the current cell is alive and the current cell is dead. Then I replaced the if statements in the old version with a switch statement, hoping that they perform faster. I guess this brought not the desired result.

The last part in the do..while loop is redrawing the screen where the new generation has a different value than the old generation, and lastly setting the current generation to the new generation:

2dlife v1, redrawing the screen2dlife v2, redrawing the screen
if(screenX[x][y] != screenY[x][y]) {
poke(char, SCRMEM+x-1+(MAXX*(y-1)),
screenY[x][y]);
screenX[x][y] = screenY[x][y];
}
if(sX[row][col] != sY[row][col]) {
switch(sY[row][col]) {
case 1:
poke(char, SCRMEM+col-1+(MAXC(row-1)), ALIVE);
break;
default:
poke(char, SCRMEM+col-1+(MAXC(row-1)), DEAD);
break;
}
sX[row][col] = sY[row][col];
}

Again I replaced the if statement in the old version with a switch statement, again this did not really speed up the program.

Next I wrote a short and lean test program that uses an int array with the dimension 24×40. After setting each int to zero, the program goes into a do..while loop, sets each int randomly to either 0 or 1, and then pokes each int directly to screen memory. Lastly, the int values are copied over to the second array. This is a good portion of my version 2 code and execution takes around 5.7 seconds when measured using the Jiffy clock. I found out that if I leave the statement out to set the array with random zeros and ones

sX[row][col] = abs(random() % 2);

the total time goes down to 3.2 seconds! So it looks like random( ) is kind of a CPU-hungry function. This is the explanation why my test code is even slower than version 1 and 2 of the 2D Game for Life.

Now I replaced the POWER C random( ) function with my own, using voice channel 3 on the SID chip to generate random numbers between 0 and 1:

#define VOICE3L 0xd40e
#define VOICE3H 0xd40f
#define VOICE3C 0xd412
#define RAND 0xd41
/* randomSID() - generate random number
  between 0 and 255 using the SID
  sound generator 
*/
int randomSID(void) {
  poke(char, VOICE3L, 0xff);
  poke(char, VOICE3H, 0xff);
  poke(char, VOICE3C, 0x80);
  return(peek(char, RAND));
}
...
        if(randomSID() < 127)
         sX[row][col] = 0;
        else 
         sX[row][col] = 1;
...

My new function is definitely faster:

test code using random( )test code using SID sound generator
5.7 seconds4.9 seconds

With this new function I created version 2.1 of my 2D Game of Life: https://github.com/jklingel/Game-of-Life/blob/main/2dlife-c64-2.1.c. With slightly above 5 seconds between generations this is a small improvement but still too slow. I could go back to a char array and hope that this will shave off another second, but after spending too many hours on this code already, I call it a day.

Overall, I have to admit that working with POWER C is not a lot of fun. Not only is the code too slow for some applications, POWER C likes to crash once in a while. In one hour of coding and compiling I had an average of 1-2 crashes. The last bug in my list of many is this: The linker created the file t3.sh just fine, but the shell claims the file does not exist and therefore does not execute it:

I could start replacing CPU-hungry functions with assembly code and use the POWER C sys( ) function to call the machine code directly, but I guess this is not worth the effort. Maybe I look into using Visual Studio Code and VS64 next to compile the code using a modern IDE and compiler.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Der Zeitraum für die reCAPTCHA-Überprüfung ist abgelaufen. Bitte laden Sie die Seite neu.