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.