Skip to content

Conversation

davepagurek
Copy link
Contributor

Resolves #8017
Resolves #8013

This implements some of the changes discussed in the above two issues.

Previously, this was the performance profile of time spent in functions:
image

After these changes, it looks like this:
image

According to those, the largest problems were:

  • Matrix constructors
  • Vector constructors
  • getters (I saw two: primarily the global binding getter, and to a lesser extent, one in p5.Vector.copy)

On this test sketch, starting with a base runtime of 420ms, here are the optimizations I made and the new runtimes afterwards:

  • Remove the extra array copy from p5.Matrix constructor: 400ms
  • Removes the Matrix interface method checks from the constructor: 390ms
  • Caching bound functions on p5.prototypeand constants that we make global: 315ms
  • Removal of extra get()s in p5.Vector (using .values when ._values is the same): 299ms
  • Removal of extra .map()s in p5.Vector constructor: 292ms
  • Removal of dynamic typeofs + caching bound functions in global non-p5.prototype functions: 250ms

For comparison, when using 1.11.0, the performance is 215ms. So this is still not quite at 1.x perf, but it's a lot closer.

Takeaways

Dynamic getters are generally slower than property accesses! We should consider whether they're really necessary when we use them if it's something that will be accessed a lot, e.g. a constant.

Not from these particular tests, but at work we experimented in the past with using a Proxy to wrap p5 in order to get multiple global mode sketches to be able to run in a shared environment without them knowing, but found base performance proxying all p5 methods to be significantly worse. Rewriting the sketch code to use direct accesses gave comparable performance to regular global mode, despite the proxy methods all being pretty simple. So Proxies aren't an across-the-board replacement for dynamic getters either without measuring. In general we should just try to make as much as possible non-dynamic when it isn't necessary.

src/core/main.js Outdated
Comment on lines 122 to 140
const boundFunction = p5.prototype[property].bind(this);
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
get() {
return boundFunction;
},
set: createSetter()
});
} else if (isConstant) {
// For constants, cache the value directly
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
get() {
return constantValue;
},
set: createSetter()
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for this part, we can branch it and optimize more when FES is disabled, since the getter is just returning the value and the setter is just binding the FES overwrite message (which do we need provided we have the sketch verifier?), we can check if FES is not enabled then do a more direct binding.

if(p5.disableFriendlyErrors){
  Object.defineProperty(window, property, {
    configurable: true,
    enumerable: true,
    value: boundFunction
  });
}else{
  Object.defineProperty(window, property, {
    configurable: true,
    enumerable: true,
    get() {
      return boundFunction;
    },
    set: createSetter()
  });
}

From my test, it should be equivalent in performance to instance mode.

Copy link
Member

@limzykenneth limzykenneth Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My tests also consistently have 2.x being slightly faster than 1.x with these relevant optimizations or just comparing instance mode. I mainly use console.time() console.timeEnd() just so that I'm not using anything p5.js internal to do the measurement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea! added some more branches for that.

@davepagurek davepagurek merged commit 1a86985 into dev-2.0 Sep 17, 2025
5 checks passed
@davepagurek davepagurek deleted the global-function-perf branch September 17, 2025 14:31
@limzykenneth
Copy link
Member

limzykenneth commented Sep 18, 2025

@davepagurek I was just testing this and I found that 4e145ca has broke parameter validation as it currently only works with I think functions within the core modules only. I'm planning on changing the way parameter validation function decoration works as I have figured out why it has so much performance hit but need to solve this first.

Edit: Caching the bound function itself seems to be the problem as the cached bound function is detached from p5.prototype with a copy at that point so when something else comes along and tries to modify the function (which parameter validation does) it does not have an effect.

@davepagurek
Copy link
Contributor Author

davepagurek commented Sep 19, 2025

@limzykenneth could this possibly be an ordering issue? FES currently does the function validation wrapping in presetup, could we move the global binding to presetup too so that the order is like:

  • FES presetup
  • everything else?
  • global binding comes last

...since addons presumably shouldn't be directly using globals anyway, and user sketch code should only be happening in setup onwards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants