Slowdown in Game of Life with Objects - Why?

I am working on recreating Game of Life in different ways, taking Coding Train’s video as the starting point. What I am trying to add is ‘color inheritance’: A newborn cell ‘inheriting’ the color from the average of the neighbouring cells’ colours.

I have made two versions: The first one has a 2D grid array for cells, and each cell is an array with RGB values.

grid[i][j] = [255,0,100]

A for loop iterates through the grid array and draws boxes of the right colour on the right position on the canvas. It then checks if the cell should be alive or dead next generation. This works fine without any slowdown, no matter the size of the grid.

https://editor.p5js.org/umutreldem/sketches/1qCmEzHlY

The second version fills each element of the grid array with Cell objects. This object receives its coordinates on the grid and its size. The color is given relative to its position on the grid.

function Cell(x, y, size) {
this.x = x * size;
this.y = y * size;
this.size = size;

//...

this.r = x * 10;
this.g = y * 10;
this.b = x+y;

The Cell object has functions for determining how many alive neighbours it has, inheriting colour from neighbours, etc. It is the same process as the first version. Then in draw() the grid is looped twice: once for determining the next status of each cell, and once for changing the cells to their new state.

function draw() {
  background(0);
  
  
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      grid[i][j].show();
      grid[i][j].evolve(countNeighbors(grid, i, j), i, j, grid);
    }
  }
  
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      grid[i][j].swap(i, j, grid);

    }
  }
}

This version experiences massive slowdown, especially at higher resolutions.
https://editor.p5js.org/umutreldem/sketches/2skO6-2Cm

I would like to add more features to this, such as keeping count of the age of each cell (for how long it is alive,) and using making an object for each cell makes more sense to me.

With my limited experience with p5.js / JavaScript in general, I cannot understand why this is the case. The function of both versions is the same (apart from one extra loop in the second version), so why does the second version tax the computer this much?

Hello @umutreldem,

Finding performance bottlenecks can be quite a puzzle sometimes. There’s more than one strategy as well, my go-to is using the profiling capabilities in the browser’s web developer tools. But let’s try a manual way for your sketch.

First, make an educated guess. Probably, the slow code resides somewhere inside draw. Why? Because it runs 60 times per second on most machines. If code inside that function is slow, it will have huge effects on the overall performance.

Next, lets start measuring:

function draw() {
  let start = millis();
  
  background(0);
  
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      grid[i][j].show();
      grid[i][j].evolve(countNeighbors(grid, i, j), i, j, grid);
    }
  }
  
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      grid[i][j].swap(i, j, grid);
    }
  }
  
  print(millis() - start);
}

The line print(millis() - start) will print how many milliseconds each call to draw() take. On my machine, about 110 milliseconds. That’s way to slow, as it should take 16,7 milliseconds or less to reach 60 frames per second.

By commenting out lines of code, we can try and find the culprit. I found that when commenting out this line:

//grid[i][j].show();

the time spent in each draw went from around 110 milliseconds down to about 4 milliseconds. Now we are gettings somewhere, cell.show() is to blame for the slowdown. Drawing can be expensive, and the code is drawing quite a lot of squares. 19 200 each frame adds up to 1 152 000 squares each second. Or, it would have, if it weren’t for the slowdowns.

So, if you want to make the sketch faster, the part of the code you should optimize is around the drawing. It’s probably not what you want, but, as an experiment, try drawing single pixels instead of squares. That is probably going to improve performance a lot.

  // Example: draw pixels instead of squares.
  this.show = function() {
    if (this.status == 1) {
      set(this.x, this.y, color(this.r, this.g, this.b));
    } else {
      set(this.x, this.y, 20);
    }
  }
function draw() {
  background(0);
  // [...]

  // For the pixel example to work, you have to call this function at the end in `draw()`.
  updatePixels();
}

The above changes make the sketch render at 60 frames per second on my machine.

Random reading tip: Optimizing p5.js Code for Performance.

1 Like

Fantastic answer and resources Sven, thank you! I will make sure to give the article a good read.

With your help I finally figured the main problem: The second version draws every square, even black ones for dead cells:

    if(this.status == 1) {
      fill(color(this.r, this.g, this.b));
      stroke(0);
      square(this.x, this.y, this.size);
      } else {
          fill(20);
          stroke(0);
          square(this.x, this.y, this.size);
      }
    }

Whereas the first one only draws the live cells, which makes more sense:

this.show = function() {
  
  if(this.status == 1) {
    fill(color(this.r, this.g, this.b));
    stroke(0);
    square(this.x, this.y, this.size);
    }
  }

Changing this did indeed render the sketch at 60 fps.

2 Likes