Member Access in JS vs C++

The syntax of C++ and JavaScript are similar. This is especially true for code that iterates over arrays or accesses members of class instances.

But the performance can be different because of differences in the internal data models. You should be aware of some general rules to help your JavaScript code run fast.

  • In C++, member access is a compile time memory offset, it's free.
  • In C++ exposed to JavaScript, all members are actually getter functions that dynamically create the result on every reference.
  • If the member you're accessing is large like Array<vec3>, cache the result in a local variable.
  • When possible, iterate using new for..of syntax instead of old style for(;;) syntax.

C++ Member Access

In C++ classes are used as containers to store multiple values in a single place. For example consider the following class:

struct Point {
float x;
float y;
};

In memory this is stored using a single allocation that is big enough to fit both floats. When code accesses one of the members, the compiler will statically emit code that knows the exact offset into the data structure.

// Allocate an instance on the stack.
Point point{};
// Writes are direct writes to the address of `point` + the static offsets of the members.
point.x = 10.0;
point.y = 20.0;
// Likewise, reads are performed with zero overhead using offsets from the stack allocated
// address.
std::cout << "x=" << point.x << " y=" << point.y << std::endl;

C++ can also nest structures and arrays inside other structures and arrays. Consider the following:

struct Triangle {
std::array<Point, 3> vertices;
};

We can create an instance of Triangle and a single allocation will be made that holds all 6 floats in linear memory space.

Triangle triangle{};
// Each complex lookup is resolved to a single static offset into the memory address.
triangle.vertices[0].x = 0.0;
triangle.vertices[0].y = 0.0;
triangle.vertices[1].x = 10.0;
triangle.vertices[1].y = 10.0;
triangle.vertices[2].x = 20.0;
triangle.vertices[2].y = 0.0;

So if we wanted to loop over the vertices printing each pair, it could look something like this:

for (int i = 0; i < triangle.vertices.size(); i++) {
std::cout << "i=" << i
<< " x=" << triangle.vertices[i].x
<< " y=" << triangle.vertices[i].y
<< std::endl;
}

Again, the complex lookup expressions like triangle.vertices[i].x are compiled away to a single static offset added to the pointer address. The triangle.vertices.size() expression in the for loop header is a constant value.

C++ Member Access From JS

We can consume this C++ struct from JavaScript through V8 bindings similar to the lumin module in MagicScript. The syntax can look nearly identical, but the performance cost of lookups is different.

Suppose we have a native function exposed to JS that gives us a triangle:

import { getTriangle } from "geometry";
let triangle = getTriangle();

In the API docs for the bindings, Triangle.vertices is a member of type Array<Point>. The std::array in C++ mapped to a normal JavaScript Array instance.

We can loop over this using similar syntax as C++:

for (let i = 0; i < triangle.vertices.length; i++) {
console.log("i=" + i
+ " x=" + triangle.vertices[i].x
+ " y=" + triangle.vertices[i].y
+ "\n");
}

But since triangle is a wrapped native object and triangle.vertices triggers a dynamic call to C++ which returns a fresh array of points every time, the performance is different!

Cache Expensive Member Accesses

It would be better to cache the result of triangle.vertices into a JavaScript local variable so it's only called once and then iterate over that native JavaScript array.

let vertices = triangle.vertices;
for (let i = 0; i < vertices.length; i++) {
console.log("i=" + i
+ " x=" + vertices[i].x
+ " y=" + vertices[i].y
+ "\n");
}

This is better, but just like modern C++, JavaScript also has modern iteration syntax that we can use. This new syntax is clearer and performs better since the built in iteration protocol in JavaScript will cache the result of triangle.vertices for us.

for (let vertex of triangle.vertices) {
console.log("i=" + i
+ " x=" + vertex.x
+ " y=" + vertex.y
+ "\n");
}

Real World Use Case

A real world use case of this would be iterating over the WorldPlaneCastResult to draw the planes as line nodes.

In the docs, it states that lumin.WorldPlaneCastResult.Plane has a member .vertices with type Array<vec3>. We could iterate over this the same as is done in the official C++ example, but it will perform poorly.

// For each plane in the results, create line nodes...
for (let i = 0; i < planeData.vertices.length; i++) {
node.addPoints(planeData.vertices[i]);
}

In testing there were typically about 13 planes with many vertices each totaling over 1000 vertices.

With this direct port from the C++ syntax, it was taking over 5000ms to render a single animation frame!

The problem is that every time JavaScript references planeData.vertices the C++ code will create a new JavaScript array, iterate over all the vertices in the C++ side and create nested arrays of 3 float values for each point.

This is especially wasteful in the case of the for loop head where we're creating thousands of arrays just to lookup the length of the array.

Refactoring to use for..in syntax fixes the performance issues and the code looks better.

// For each plane in the results, create line nodes...
for (let vertex of planeData.vertices) {
node.addPoints(vertex);
}