Accessing width and height variables much slower in P5 Ver 2.0 compared to Ver 1.0

Problem: There seems to be a big discrepancy in performance

I recently came back to P5 after not having used it for a few years. I’m just coding.

I grabbed the latest version (2.3.2) and wrote a quick game of life program. It seemed really slow compared to what I expected.
Long story short: I dug out an old version I had written years ago in P5 and compared. After recoding the new one to be code-identical to the original and trying different versions of P5, I discovered that the same GoL program runs about 20 times slower in Ver 2. (60fps in V1, 4fps in V2)

So I started commenting out and removing code to try and find the bottleneck. Below is the minimalist example which exhibits the problem.

let t1, t2;

function setup() {
  createCanvas(1024, 640);
}

function draw() {
  background(200);

  t1 = millis();

  for(let y=0; y<height; y++) {
    for(let x=0; x<width; x++) {
      // Do nothing
    }
  }

  t2 = millis();

  const elapsed = t2 - t1 // V.1.11.13 = \~0.3msec, V.2.3.2 = \~17.5msec
  text(elapsed.toFixed(2), 3, height-3);
}

In ver. 1.11.13 this takes ~0.3 msecs
In ver. 2.3.2 this takes ~17.5 msecs

That’s almost 60 times more. I’m simply doing two nested for loops over all the pixels, without doing any computation on the pixels. So this is effectively just JavaScript with no P5 functions in the loop, so I don’t understand why it’s so slow.

Is there something I have to do different in the setup of version 2.0? Or is Version 2.0 doing something in the background to slow things down. Any pointers would be great as I’m completely stumped. I tried searching on this forum, but only found references to a couple of graphics functions which were much slower in V2.0.

Extra Detail:
I’m using the minified version in both instances, so it shouldn’t be anything to do the friendly errors.
I’m on Windows, testing in Chrome.
Everything is consistent between the tests other than the P5 version.

Update:
Sorry for the long post, but typing this out actually gave me an idea and I’ve partially answered my question: If I substitute the width and height variables in the loops for hard coded numbers then the loop speeds up to match Ver 1.0. Similarly, I can alias the width and height variables and achieve the same result. So it appears to be something to do with accessing the width and height variables directly.

Very interesting result so I decided to try it out on my computer.

I made some small changes in your code

  • I have added a simple task in the double loop to ensure the elapsed time >= 1ms
  • I have added a moving average calculator so it displays the average time over the last 120 frames

I tested it using

  • Firefox on my iMac
  • p5.js v1.11.13 and 2.3.2
  • using width / height properties and WIDTH / HEIGHT constants

In all cases there is significant fluctuation almost certainly due to other tasks being performed by the OS so I have reported the results to the nearest millisecond. If you try this program allow it to run for a couple of minutes keeping your hands off the keyboard and mouse :grinning:

   +-----------------+-----------+------------+
   |       VERSION   |  1.11.13  |   2.3.2    |
   +-----------------+-----------+------------+
   | width / height  |   ~3ms    |   ~12ms    |
   -----------------+-----------+-------------+
   | WIDTH / HEIGHT  |   ~3ms    |   ~3ms     |
   |-----------------+-----------+------------+

As you say it suggests that V2 retrieves the display width/ height differently from V1 which agrees with what you discovered, well spotted :+1: :clap:

const WIDTH = 1024, HEIGHT = 640;
let t1, sum;
let ma;

function setup() {
    createCanvas(WIDTH, HEIGHT);
    textSize(50);
    ma = new MovingAverage(120);
}

function draw() {
    background(230);

    t1 = millis();
    sum = 0;

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            // give it something to do to ensure elapsed time is >=1 ms
            sum *= x * y;
        }
    }

    ma.submit(millis() - t1);
    text(ma.average.toFixed(2), 10, height - 60);
    text(`Version ${VERSION}`, 30, 60);
}

class MovingAverage {

    constructor(buffer_size = 2) {
        if (Number.isFinite(buffer_size))
            this._size = Math.max(2, buffer_size);
        else
            this._size = 2;
        this._buffer = new Float64Array(this._size);
        this.reset();
    }

    get average() { return this.avg }

    reset() {
        this.avg = this.sum = this.ptr = 0;
        this._buffer.fill(0);
        this.submit = this.__submitImpl_PartFilled;
    }

    submit(v) { }

    __submitImpl_PartFilled(v) {
        this.sum += (this._buffer[this.ptr] = v);
        this.ptr++;
        this.avg = this.sum / this.ptr;
        if (this.ptr == this._size) {
            this.ptr = 0;
            this.submit = this.__submitImpl_Full;
        }
    }

    __submitImpl_Full(v) {
        this.sum -= this._buffer[this.ptr];
        this.sum += (this._buffer[this.ptr] = v);
        this.avg = this.sum / this._size;
        this.ptr = (this.ptr + 1) % this._size;
    }

}


Thanks for the response and for taking the time to code up a more comprehensive example to validate the problem. Good idea to give the loop something to do, it makes the issue not look quite as severe, but still roughly 4 times slower.

It’s late where I am now, but I will revisit this tomorrow. I’ll see if I can find the points in the P5 library where the width and height variables are stored to see if there is a difference between the versions.

I’ll also go through the game of life code and change all the references to width & height to WIDTH & HEIGHT aliases to see if that brings the performance back up to V1.0 speeds.

I updated the title to better reflect the current understanding of the problem.

Previous: For loops very slow in P5 Ver 2.0 compared to Ver 1.0

New: Accessing width and height variables much slower in P5 vVer 2.0 compared to Ver 1.0

To avoid the need for global WIDTH and HEIGHT constants a better solution would be this for the double loop.

    for (let y = 0, H = height; y < H; y++) {
        for (let x = 0, W = width; x < W; x++) {
            // give it something to do to ensure elapsed time is >=1 ms
            sum *= x * y;
        }
    }

With 2.3.2 we get back to 3ms because we are only reading the width and height properties once and using the W an H variables each loop iteration to terminate the loop at the right time. This has the added advantage that W and H are only visible to the code inside the outer loop avoiding unwanted side-effects.

Although this does not negate the issue with 2.3.2 it is fairly standard practice for instance looping through and array would be

for(let i = 0, LEN = array.length; i < LEN; i++){
    // do something with the array element
}

Version 2 width & height:

Version 1 width & height:

Mitigation:

function draw() {
  const { width, height } = this; // width & height are cached as local vars now
}

Gemini AI explanation:

Accessing getters repeatedly inside tight loops incurs a performance penalty in JavaScript because property lookups with getter functions cannot always be optimized or inlined by engine JIT compilers (like V8) the same way static primitive properties can.

Why Getters Slow Down Loops

  • Function Overhead: Every time you read a property backed by a getter, the JavaScript engine must execute a function call behind the scenes (the get trap) rather than instantly reading a memory address containing a static value.
  • The Global-Mode Extra Layer: In p5.js global mode, accessing a variable like width triggers a global proxy getter on window, which then evaluates instance.width, which in turn executes the renderer getter checking this._renderer?.width (including optional chaining overhead).
  • Missed JIT Optimizations: Inline Caches (ICs) in browsers can optimize property access, but dynamic accessors, optional chaining, and context-bound calls frequently cause megamorphic states or prevent full property inlining when hit thousands of times per second.

Pulling dynamic dimensions into local variables once per frame or once per loop execution entirely bypasses the repetitive getter and binding overhead.

Thanks, that’s a good solution that I hadn’t thought of. I will update my game of life code and see how it goes.

Thanks GoToLoop, good to know the actual underlying cause. Although I’m surprised that they’ve taken this route considering how much impact it has on two variables that are used so ubiquitously.

There are many tutorials on the web, such as The Coding Train, where width and height are utilised consistently in loops over all the canvas pixels.

It should probably be written into the documentation that if you are accessing these variables in a tight loop then the standard practice would be to alias them.

I’m suspicious the worst offender is the operator ?. at: this._renderer?.width

Maybe a regular dot . operator would allow JS to optimize those getters:
this._renderer.width

B/c IMO it doesn’t make sense for width & height to be absent ever. So why the ?. for?

Anyways, I believe const { width, height } = this; should make p5js v2 close to v1 performance.

Maybe even p5js v1 could get some tiny performance using that mitigation too; b/c local variables are fastest.

You can even extend the workaround for other system variables, like mouseX and pixels[] too:

function draw() {
  loadPixels();
  const { width, height, mouseX, mouseY, pixels } = this;
}

I tested this by editing the P5 library file to remove the ?'s. But it didn’t make a noticeable difference. But I do agree that the ?'s are probably not necessary as width & height should always be present. So it must just be the overhead of using a getter rather than a directly accessible variable.

For my game of life program, I have a grid of 1024x640 and am using a single array for the current and next generations rather than nested arrays. As I mentioned, this runs at a consistent 60fps in V1, but 4fps in V2.
The first thing I tried was const {width, height} = this but that didn’t make any difference. Then I realised I had a function call getCell, which was called 8 times per pixel to get the neighbours:

const neighbours = getCell(x - 1, y - 1) + getCell(x, y - 1) + getCell(x + 1, y - 1) +  getCell(x - 1, y) + 0 + getCell(x + 1, y) +  getCell(x - 1, y + 1) + getCell(x, y + 1) + getCell(x + 1, y + 1);

This function looks like this:

function getCell(x, y) {
  return current[y * width + x];
}

So that was the bigger culprit, calling width inside that function 8 times per pixel over 655K pixels per frame had a bigger impact than the loops.

No way to alias that efficiently within the function so I created global variables rather than pass a width in for every call. It’s still not quite as fast as V1. In V1, to update and render every pixel takes ~11ms. In V2 it takes ~17ms with the exact same code, but that’s negligible in the grand scheme of things.

Anyway, thanks to yourself and Quark. I’ll mark your answer from Gemini as the solution as that seems to be the culprit. Hopefully this thread will serve as help for newbies that run into this issue. I can see someone following a Coding Train video and wondering why their code is running so much slower.

I asked a p5.js maintainer. They said:

It is a known limitation, the trade off is a slight performance hit by using getters in exchange of not having to keep track and potentially desync the value of width and height when core code base of addon code changes the renderer’s size. ie. In 1.x when an addon wanted to manipulate the renderer size, they have to also remember to manipulate the global width and height value or the user won’t get the right value.

We can reconsider this if the performance hit is really significant.

the other thing is that different browsers optimise things differently so it would be great if there are suggestions on alternative approach that we can take, we’ll need to look into that as well.

A more optimized array access code:

const { width, height } = this;

const rowAbove = width * (y - 1);
const rowIndex = width * y;
const rowBelow = width * (y + 1);

const curr = current;

const neighbours = 
  curr[rowAbove + x - 1] + curr[rowAbove + x] + curr[rowAbove + x + 1] + 
  curr[rowIndex + x - 1]                      + curr[rowIndex + x + 1] + 
  curr[rowBelow + x - 1] + curr[rowBelow + x] + curr[rowBelow + x + 1];

Also, replacing current[]'s vanilla array w/ a typed 1, like Int8Array can also help on performance:

Thanks @GoToLoop , I’m sure there are numerous ways to optimise my code. My original intention was just to put something together quickly to jump back into P5 programming for fun. And then I concentrated on the difference in execution speed between V1 & V2 rather than trying to optimise my code as it already ran at 60fps on a large canvas in V1.

Good suggestion with the typed array. I couldn’t use an INT8Array in this instance as I’m cheating and populating the array with true/false rather than 1/0 and taking advantage of the fact JavaScript will add true & false together mathematically as if they were 1 & 0. Even then, I’m not sure if the typed array would be that much faster as I recently played around with a UINT16Array for a 900K+ (1280*720) array for a Mandelbrot generator in vanilla JavaScript, and there was no measurable performance improvement over a standard array.

In JS and many other languages, true & false are stored internally as 1 & 0.
As you’ve already noticed, JS will coerce true to 1 and false to 0 when using a typed array.

Still it’s worth a try. The actual advantage is that a typed array is guaranteed to store all of its values sequentially in 1 contiguous block of allocated memory.

If we later access a typed array as sequentially as we can, CPU will be able to cache it for ultra speed reading.

I did not know that! I will give it a try tomorrow, just for fun :slightly_smiling_face:

@sableraph thanks for bringing it to the attention of one of the maintainers. That explains the reasoning behind it. I would think that if you weren’t calling width and height so many times per frame then the performance hit wouldn’t be noticeable.

The fact that no one else has brought this up suggests that it isn’t noticeable in the majority of sketches. I’ve got quite a few old V1 sketches which were built from YouTube tutorials. I’ll have a look through some of them to see if there are any that would be heavily impacted by this change.

I have been programming for many decades and during that time I have created many programs demonstrating Conway’s Game of Life (in 4 different languages, BASIC, Pascal, Java and JavaScript).

The early versions were very slow because my code was a literal I was too literal in converting the verbal description GoL into code so I would like to pass on what I have discovered about optimisation.

  1. Tweaking the source code rarely makes a significant difference unless the original code was poorly written. (I have been there, done that … :innocent:)
  2. Selecting the data structures and designing the algorithms appropriate for the task makes a huge difference on the application performance.

In my last GoL version I was getting a generation calculation time of ~12ms and a frame rate ~58 fps for a grid of 4 million cells.

JavaScript uses dynamically typed data so although you can use a standard array for numeric data I suspect typed-arrays would make it easier for the JIT optimiser to do its stuff :grin:. They also have an advantage when using web-workers to create multicore programs.

I think we’re going down a bit of a rabbit hole compared to the original intent of the post :slightly_smiling_face: .

But I do stand corrected, there is a noticeable speed improvement using a typed array. I tested it with a vanilla JavaScript version of the code as that comfortably runs at 60fps with a 160081000 grid. An Int8Array provides greater than 20% speed improvement during the update portion, i.e. updating to the next generation. There’s no noticeable improvement in the render portion when reading the values from the updated array, but that’s already running sub 4ms and is probably gated by updating the imageData.data array and call putImageData.

I went through a few old V1 files and it was really only sketches that make use of the entire pixel grid that caused any real issues. The Mandelbrot set was another good example. Take the following code snippet, which is rendering a pre-calculated Mandlebrot set from an array called ‘fractal’ on a 640x640 canvas:


  for(let y=0; y<height; y++) {
    for(let x=0; x<width; x++) {
      const f = fractal[y * width + x];
      const i = (y * width + x)*4;
      const c = (f/maxIterations)**.5*255|0;
      
      pixels[i] = c;
      pixels[i+1] = c;
      pixels[i+2] = c;
      pixels[i+3] = 255;
    }
  }
  updatePixels();

Ver 1.x renders this in just over 1msec. Ver 2.3.2, by default, renders this in 80msecs.
Adding const {width, height, pixels} = this brings that back down to equal ver 1.0. Caching the pixels array has the biggest impact here as it is referenced 4 times per pixel. (Interestingly changing the arrat to an Int8Array had no impact on either the calculation time or the render time).

Anyway, I’d love to continue this over a beer or a coffee. But I think we all understand the problem now and hopefully this thread will help anyone that comes across the problem.
Thank you all for your input and help.

p5js’ pixels[] is already an Uint8ClampedArray:

At least, after calling loadPixels():

I knew that. I meant using an Int8Array to store the fractal data rather than using an ordinary array.:slightly_smiling_face: