Skip to content
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

isVector3 is present when serializing Vector3 #24167

Closed
olivierchatry opened this issue Jun 1, 2022 · 39 comments · Fixed by #24219
Closed

isVector3 is present when serializing Vector3 #24167

olivierchatry opened this issue Jun 1, 2022 · 39 comments · Fixed by #24219

Comments

@olivierchatry
Copy link
Contributor

olivierchatry commented Jun 1, 2022

Describe the bug

A clear and concise description of what the bug is. Before submitting, please remove unnecessary sections.

since r141, when serializing Vector3 class using JSON.stringify, isVector3 is present

To Reproduce

Steps to reproduce the behavior:

  1. Go to '...'
  2. Click on '....'
  3. See error

Code

// r141
console.log(new THREE.Vector3())
// output:
// {isVector3: true, x: 0, y: 0, z: 0}

// r140
console.log(new THREE.Vector3())
// output:
// {x: 0, y: 0, z: 0}

Live example

*141

*140
Expected behavior

Should not contains isVector3 ( also, I don't think it is a good idea to have a boolean inside an heavily use class memory wise )

Screenshots

image

Platform:

  • Device: [Desktop, Mobile]
  • OS: [Windows, MacOS, Linux, Android, iOS]
  • Browser: [Chrome, Firefox, Safari, Edge]
  • Three.js version: [dev, r???]
@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 1, 2022

This is duplicate of #24093.

Serializing math objects should not be done via JSON.stringify(). Please use toArray() and fromArray() interface.

@olivierchatry
Copy link
Contributor Author

Well, that will require a LOT of change in my code. a LOT.
And I'm still not sure why you need a boolean to indicate if it is a Vector3 or not. This seems really really unnecessary.

@mrdoob
Copy link
Owner

mrdoob commented Jun 1, 2022

I think we can solve this by adding a toJSON method to Vector3:

	toJSON() {

		return { x: this.x, y: this.y, z: this.z };

	}

https://jsfiddle.net/8wr47h1m/

@olivierchatry
Copy link
Contributor Author

That is one way of solving it for sure ( will probably slow down the process of serialization, which is a different issue ).

But I'm really curious about the need for a isVector3 boolean ?

@LeviPesin
Copy link
Contributor

Because every "object" class in three.js has such boolean.

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 1, 2022

Read #24047 for more details.

@mrdoob
Copy link
Owner

mrdoob commented Jun 1, 2022

@olivierchatry

That is one way of solving it for sure ( will probably slow down the process of serialization, which is a different issue ).

Could you do performance test?

@olivierchatry
Copy link
Contributor Author

@mrdoob will do asap ( will probably do that tomorrow, have to work on something today ).
@Mugen87 I understand that in the prototype it seems "free" memory wise, I'm not sure CPU wise if the access is slower or not ( have no clue about how v8 or SpiderMonkey handle access to attribute in prototype ).

The real question is why you need as isVector3, this seems rather odd, but maybe there is a use case I do not see.

@olivierchatry
Copy link
Contributor Author

@LeviPesin ok but how is it use ?

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 1, 2022

The real question is why you need as isVector3, this seems rather odd, but maybe there is a use case I do not see.

There are occasions where we have to test for the object's type. We could do this with the instanceof operator however that would require to import the actual type/class which is bad for tree-shaking, see #24006 (comment).

@olivierchatry
Copy link
Contributor Author

olivierchatry commented Jun 1, 2022

I honestly have a really really hard time getting my head around that. For me, the less attribute you have in an heavily use class the better. I can understand for "hi level" object, but for math object, I think it should be kept to the bare minimum. But then again, it is a reflexes I had when I was using C++, so it might really not being relevant here. It just feels really wrong.

Also, "there are occasion" is the root of all evil :).

( I'm benchmarking the toJSON solution now as I'm really curious about it ).

@olivierchatry
Copy link
Contributor Author

olivierchatry commented Jun 1, 2022

Yup HEAVY hit :

import { Vector3 } from "./Vector3.js"


const array = []

let i 
for (i = 0; i < 100000; ++i) {
  array.push(new Vector3(Math.random(), Math.random(), Math.random()))
}

const start = performance.now()
for (i = 0; i < 100; ++i) {
  JSON.stringify(array)
}

const delta = performance.now() - start

console.log(`took ${delta} ms`)

Without toJSON:

❯ node test.mjs
took 4962.212369999848 ms
three.js/src/math on  dev [?] with  took 5s
❯ node test.mjs
took 4958.565330000594 ms

With toJSON:

❯ node test.mjs
took 5927.112598000094 ms
three.js/src/math on  dev [!?] with  took 5s
❯ node test.mjs
took 6188.454107999802 ms

@olivierchatry
Copy link
Contributor Author

@Mugen87 there is no performance impact adding isVector3 as member ( tested with this code ):

import { Vector3 } from "./Vector3.js"


const array = []

let i 
for (i = 0; i < 100000; ++i) {
  array.push(new Vector3(Math.random(), Math.random(), Math.random()))
}

const len = array.length

const start = performance.now()

for (let j = 0; j < 10000; ++j) {
  for (i = 1; i < len; ++i) {
    array[i - 1].add(array[i])
  }
}

const delta = performance.now() - start

console.log(`took ${delta} ms`)

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 1, 2022

Even if a custom toJSON() method introduces an overhead, the performance seems still acceptable. If @mrdoob is okay with it, I can make a PR and update all vector classes.

@olivierchatry
Copy link
Contributor Author

Performance wise, it is not super good, at least for my personal use case ( I'm using serializiation on backends, generating a good amount of points ).

It is definitely better than nothing though, even if I really don't get the need for a boolean inside a vector class.

Thanks a lot for the help !

@mrdoob
Copy link
Owner

mrdoob commented Jun 2, 2022

@Mugen87

If @mrdoob is okay with it, I can make a PR and update all vector classes.

Sounds good! 👍

@mrdoob
Copy link
Owner

mrdoob commented Jun 2, 2022

@olivierchatry

Screen Shot 2022-06-02 at 1 11 37 PM

@olivierchatry
Copy link
Contributor Author

@mrdoob the good old grep ! could have use that.

Why not use static member ?

class Vector3 {

  static isVector3 = true;
  ...
}

const a = new Vector3()
console.log(a.constructor.isVector3)
> true
console.log(a)
> Vector3 { x: 0, y: 0, z: 0 }

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 2, 2022

This has been discussed before, #21285 (comment)

Static properties do now allow a.isVector3 which is the actual usage of is* properties. They are never called over the prototype reference.

@olivierchatry
Copy link
Contributor Author

olivierchatry commented Jun 2, 2022

I agree it is not a substitute, but it seems like way better to define a static, and access it through the constructor then adding a member ?

I do understand that it would require a lot of change ( since it probably mean that it needs to be implemented everywhere ), but at the same time, this seems way more clean than add a bit of memory on each vector3 ? ( I know, I'm on and on about that, but this feels really REALLY dirty to add a boolean to a vector class that will be created on each instance.

Also if I look at the grep, from @mrdoob this can probably be done another way ? )

( I'm invoking my inner Mike Acton ! )

@LeviPesin
Copy link
Contributor

LeviPesin commented Jun 2, 2022

Hm. Can a object.constructor.name work? It would not quite work with inheritance, so it may be possible to make a new method (and call it instanceof, for example :-) ), which goes along prototype chain and checks if any constructor has desired name.

So it would be like the JS's own instanceof (i.e. it would not require .is booleans) and it would work with tree-shaking.

@olivierchatry
Copy link
Contributor Author

Well ...
We can also use get :

class Vector3 {

  get isVector3() {
    return true
  }
  ...
}

const a = new Vector3()
console.log(a.isVector3)
> true
console.log(a)
> Vector3 { x: 0, y: 0, z: 0 }

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 2, 2022

I do understand that it would require a lot of change ( since it probably mean that it needs to be implemented everywhere ), but at the same time, this seems way more clean than add a bit of memory on each vector3 ?

The migration effort that results from such a change is inacceptable. Besides, the additional memory allocation of a single boolean is so minor that the resulting overhead is negligible, see #24047 (comment).

Turning the is* properties into getters has already been tried here and reverted, see #19997 (comment).

@olivierchatry
Copy link
Contributor Author

I'm not to sure about that. at the bare minimum it's one byte per item, but given the proportion of javascript to suck up memory :
Using nodejs v16.15.0

import { Vector3 } from "./Vector3.js"

const array = []
for (let i = 0; i < 1000000; ++i) {  
  array.push(new Vector3())
}

const used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`The script uses approximately ${Math.round(used * 100) / 100} MB`);

With isVector3 as an attribute :
( run multiple time, these are two min-max value )
The script uses approximately 77.86 MB
The script uses approximately 73.4 MB

Without:
( run multiple time, only value that I get )
The script uses approximately 65.65 MB

This adds about 10 mega bytes of memory for a million vector3. I would not call that negligible !

Side note, using accessor ( get version ) is way better :

The script uses approximately 65.65 MB
The script uses approximately 65.65 MB
The script uses approximately 65.65 MB

@LeviPesin
Copy link
Contributor

Side note, using accessor ( get version ) is way better :

But slow (also as my version).

I think that the public fields (proposed by @marcofugaro) is actually a good idea? It will create only one instance per class.

@gkjohnson
Copy link
Collaborator

It's a shame there isn't a way with class definitions to declare fields on the prototype...

I haven't seen this approach addressed for the toJSON use case but what if the is* properties were defined with "Object.defineProperty"?

class Vector3() {

  constructor() {

    Object.defineProperty( this, 'isVector3', { value: true } );

    this.x = 0;
    this.y = 0;
    this.z = 0;

  }

}

Not sure if there's an performance implications to the above approach but it does prevent isVector3 from getting serialized with "toJSON" meaning we can also avoid adding custom "toJSON" code to the codebase for all the math classes.

@gkjohnson
Copy link
Collaborator

gkjohnson commented Jun 6, 2022

It's also the case that if @olivierchatry's use case is considered too unique that they can override the toJSON function themselves for their own app:

Vector3.prototype.toJSON = function() {
  return { ... };
};

@Mugen87
Copy link
Collaborator

Mugen87 commented Jun 7, 2022

I haven't seen this approach addressed for the toJSON use case but what if the is* properties were defined with "Object.defineProperty"?

#21284

@gkjohnson
Copy link
Collaborator

@Mugen87 okay thanks for the reference. Last crazy idea, though - what if we set the prototype in the class constructor? I'm not sure how this would affect tree shaking and I don't love the idea but it would address both of the concerns raised in this thread (new isVector3 member per instance and json serialization):

class Vector3() {

  constructor() {

    Vector3.prototype.isVector3 = true;

    // or

    Object.getPrototypeOf( this ).isVector3 = true;

    this.x = 0;
    this.y = 0;
    this.z = 0;

  }

}

My hope would be that since the prototype access is within its own constructor that it won't affect tree shaking but I suppose that might be fairly bundler-implementation specific so maybe it's a risky assumption.

@LeviPesin
Copy link
Contributor

I think we can actually try using polymorphism and "does the object have the method we need" (duck typing) instead of using .is... See #24199 (comment).

@donmccurdy
Copy link
Collaborator

I'm worried that this thread seems pretty far down a very niche path... THREE.Vector3 does not need to be optimized around storing millions of Vector3 instances in memory — that's a terrible choice for performance regardless of this particular boolean property. This is the reason why three.js (and GPU APIs) have moved toward typed-array-based APIs like BufferAttribute instead. If you need to iterate over many vertices, a small number of vectors should be pooled and initialized, when needed, from compact binary views.

Let's keep .is properties please, they're fine. 🙏

@olivierchatry
Copy link
Contributor Author

I can of disagree with that. ArrayBuffer are actually slower access wise if you do work on the CPU ( don't ask me, it's horrible, we tested changing some of our generator to use arraybuffer, and it ended up a good chunck slower than using Vector3 ). I'm sure when WebGPU will be there we can work something out, but at least in our case Vector3 is actually faster than working against arraybuffer.

@LeviPesin
Copy link
Contributor

@donmccurdy meant using one TypedArray for all Vectors3, not a TypedArray for each Vector. It would be much, much faster.

@olivierchatry
Copy link
Contributor Author

That is what I was doing. If you want I can certainly redo the benchmark and so you can see. We event tried using indexed array instead of floating point to limit writes size. It was slower.

@olivierchatry
Copy link
Contributor Author

( did you actually do any benchmark your self ? because I was blown away myself by the result. I asked our render coder to modify our code to use typedarray so that we could leverage some webassembly, and the result were shocking to the point that I reimplemented it myself to be sure. So yeah, typedarray access on pure JS code is slower, at least in my test. )

@LeviPesin
Copy link
Contributor

Here it is:
https://jsbench.me/52l477mmlz/2
TypedArray is more than 10x faster.

@olivierchatry
Copy link
Contributor Author

This is really not a good bench. You are benchmarking the call to a vector3 constructor. Also, linear access. I'm redoing the bench I have made using simplify from there : https://mourner.github.io/simplify-js/

@olivierchatry
Copy link
Contributor Author

olivierchatry commented Jun 9, 2022

Well, never mind, we did something really stupid ( I removed the simplifyRadialDist from the typedarray buffer test ... ). I guess I'm not blown away anymore !

For science:

With simplifyRadialDist on both test ( vec3, typedarray ):

❯ node bench.js
arrayWrite 2600.320638999983 milliseconds.
vec3Write 3766.281397999992 milliseconds.
three.js/src/math on  dev [?⇣] with  took 6s
❯ node bench.js
arrayWrite 2905.3484539999918 milliseconds.
vec3Write 4216.024967000005 milliseconds.
three.js/src/math on  dev [?⇣] with  took 7s

Without simplifyRadialDist on both test ( vec3, typedarray ):

❯ node bench.js
arrayWrite 4425.489473998547 milliseconds.
vec3Write 5163.734323002398 milliseconds.
three.js/src/math on  dev [?⇣] with  took 9s
❯ node bench.js
arrayWrite 4212.7496079951525 milliseconds.
vec3Write 5430.644314996898 milliseconds.
three.js/src/math on  dev [?⇣] with  took 9s

We were comparing vec3 with simplifyRadialDist against typedarray without. Not really a fair fight. At least I now have peace of mind on that because I was really disturb when we tried.

@olivierchatry
Copy link
Contributor Author

olivierchatry commented Oct 11, 2022 via email

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

Successfully merging a pull request may close this issue.

6 participants