-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Matrix performance #12973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Matrix performance #12973
Conversation
|
Thank you for the pull request, @Beilinson! ✅ We can confirm we have a CLA on file for you. |
|
Copy of conversation from #12968:
|
|
The missing While ... but it's not unlikely that this comes at a severe performance cost (or other drawbacks that I don't have on the radar - this is just spitballing...) From an "object-oriented design" perspective I don't like that the |
| * @type {Readonly<Matrix4>} | ||
| * @constant | ||
| */ | ||
| Matrix4.IDENTITY = freezeMatrix( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently using the freezeMatrix utility rather than makeFrozenMatrix, but they are fundamentally the same. The actual matrix is just a regular object that behaves exaclty like the previous object matrix, which are 100% compatible within the codebase because I didn't change any of the behavior when using a new Float64Array matrix.
The main difference is future-proofing/avoiding accidents where someone does choose to work with them as real typed arrays (maybe for performance reasons or whatever), in which case freezeMatrix is a much more solid solution that won't accidentally break because apparently the IDENTITY and ZERO matrices dont behave like Typed Arrays (which is the case with makeFrozenMatrix4).
Unlike with a Proxy, there is no misdirection so V8 can still attempt to optimize these, albeit only to the performance level of the old matrices.
| * @returns {Readonly<Matrix4>} a frozen matrix | ||
| */ | ||
| // eslint-disable-next-line no-unused-vars | ||
| function makeFrozenMatrix4( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically just creating the old matrix type and manually binding the few added functions.
Jasmine apparently uses In consuming code and throughout there is no other way to compare matrices other than |
|
Just as a short "ack": I quickly looked over the freezing approaches. The About the equality: I'll (also) have to dive a bit deeper into that. The shallow way of phrasing my question could be: Was it necessary to change the equality check? If so, very roughly speaking: Whatever was done there (deep in Jasmine) could be done in normal client code, meaning that there is some ~"breaking change" hidden here. This would likely not be a dealbreaker, because it apparently would be an obscure corner case - but I'm usually curious about these.... |
|
Found the relevant code in jasmine, this seems pretty obfuscated to me: // Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) { return a !== 0 || 1 / a == 1 / b; }Basically they explicitly test this case because they want to compare by value exactly and not by JS regular equality comparison. This check works because If this is a noteworthy issue I guess I could add a note to the changelog? I'm really not sure that it is though. I found some stackoverflow answers saying that jasmines equality comparison was taken from |
Yeah thats basically what I felt as well, and I wrote it 😂 It could probably do with some more small well named functions, but theres a limit to how simple I can make brute force copying an objects entire prototype chain onto a new object while preventing access to specific dangerous aspects 😶🌫️ |
|
Ahh, yeah... the equality. There's always a chance to bring up https://eqeq.js.org/ One reason for why I'm asking is that some Jasmine matchers are re-wired in I knew that there was some trickery going on with EDIT: This overridden About the (I could try to explain my lack of knowledge here - either by saying that I'm a Java guy (where all this is soooo much cleaner and clearer), or by saying that nobody really knows what the mix of JavaScript/JSDoc/TSDoc and the associated tooling are actually doing. But eventually, the truth is what is written into the the |
const m : Float64Array = Matrix4.IDENTITY; //typeerror (Frozen matrix is still typed as a `Matrix4`)Both the above and below should act the same const m : Float64Array = new Matrix4(); // same typeerrorSince we also explicitly define the type of the identity and zero matrices as A user would have to explicitly cast using |
|
I'll have to leave some of the considerations here to others (or spend more time with dedicated tests and reading). But a small correction to what I said earlier: The re-defined And (iff I'm not completely wrong here), this check will return |
|
The performance improvement is impressive. It makes sense: with TypedArrays, the runtime can predict ahead of time exactly how much memory a given matrix will need. I have 3 questions:
|
|
Hey @jjhembd
I haven't heard of that library but sounds very relevant. From benchmarking I also saw that
Probably My main goal in this PR was to competely avoid breaking changes. The only way to I thought of how to do that with Cartesians would be to define all the existing The other option was to completely break the Cartesian classes and require the use of If you have any ideas how to do this without breaking changes and still improving performance that would be great however!
I also had this question, and found some very weird results.
|
|
I see that the toji/gl-matrix#453 issue also mentions the idea of getter/setters for their export class Cartesian3 extends Float64Array {
get x() { return this[0]; }
set x(value) { this[0] = value; }
get y() { return this[1]; }
set y(value) { this[1] = value; }
get z() { return this[2]; }
set z(value) { this[2] = value; }
}This is the result of testing the
Surprisingly its a net loss regardless of how we look at it |
|
It may be a bit of a tangent (at first glance), but I see some connection to #12780 here. Imagine |
|
I read about half of this thread, so disregard me if I'm not fully informed, but this comment from @javagl caught my attention:
So instead of a |
@Beilinson May chime in with additional details, but I think that some of the performance benefits could be attributed to the fact that one could still use the |
|
I think you'd have to do some testing to back that claim. I'm not so sure that's where the performance benefits are coming from - I think the main benefit is that float64array is backed by a contiguous block of memory, so access is faster and more cache friendly. Could be wrong though, that's just my hunch! As it stands, the Matrix4 class has named getters, so the performance benefit is not from the direct bracket access, then we can keep the class as-is and also save some trouble in renaming matrix usage across the cesiumjs codebase. |
|
@javagl is exactly right, the goal of the PR was to avoid any breaking changes. My original intent was to hold the My main issues with this were:
I then tried the refactor only using getters/setters on the internal array, but the performance of that was mixed - for the most part it was significantly slower than the older code. I'm not a v8 wizard, but I'd have to assume based on my reading of https://v8.dev/blog/fast-properties and some other related blog posts (https://v8.dev/blog/dataview discusses a bit of the internal
Its extremely important for me to mention that a lot of what I mentioned are assumptions from reading the above blogs, so of course seeing some real numbers would be nice: Performance ResultsI ran 4 version of 1000 randomized matrices through https://jsbenchmark.com, benchmarking
|
Not sure exactly what you meant by this, there are no named getters except for Btw, if its something important that the new syntax explicitly is a class extending |
|
Going to try to clarify my above comment. I'm talking about functions like Based on what you've said, it sounds like you were originally planning to do something like this by making You also said you fear indirect access via methods like |
|
Thanks for clarifying @mzschwartz5 , so to clarify and summarize the concerns from the rest of the team as I understand them and my stance on them:
I apologize if there are any other concerns/pointers I missed! |
The response to my concern (above) sounded convincing for me. I may have overestimated the implications - my Java backround made me think ~"Oh dear, people will do |






Description
Convert
Matrix4andMatrix3to be based onFloat64Array. This provides a significant 2x speedup of Matrix multiplication and other methods, and saves memory. This results in a 4x improvement on Model.pick and propagates throughout other areas.Matrices are at the heart of 3D graphics, and any performance improvement here radiates throughout the entire codebase.
I approached this primarily to speed up
pickModel, wheregetVertexPositionis primarily limited by the performance ofMatrix4.multiplyByPoint.By making Matrix4 a class extending
Float64Array, we get a 2x speedup in that method (as well as various levels of speedups on other methods):I show the performance improvements of this combined with @javagl 's branch #12658 in #11814.
Importantly, changing the backing datastructure to
Float64Arrayhas 100% identical behavior and results as the previous implementation. As per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#number_encoding, regular numbers in JS are also 64 bit floats. By using theclass .. extendssyntax, we continue having values accessed by doingmatrix[i], rather than forcing something likematrix.buffer[i].Testing plan
Test suite + visual comparison between sandcastles.
Author checklist
CONTRIBUTORS.mdCHANGES.mdwith a short summary of my change