Wednesday, November 28, 2012

TypeScript/JavaScript: Minimizing garbage collection when using a math library

I've spent the past few weeks porting the engine from C++/OpenGL to Microsoft's new JavaScript superset TypeScript, using WebGL to do the rendering. This lead to the development of a set of vector and matrix classes which I've released separately on Github.

The port is now mostly complete and I've started working on  performance improvements. One important aspect to remember is that JavaScript objects are always allocated on the heap (using new) and will be taken care of by the garbage collector once they're no longer needed.

In the original C++ project, I used the GLM vector and matrix library. The following is a (simplified) method from my Plane class which computes the intersection point of the plane with a ray:

class Plane
{  

   public:  

     vec3 normal;  
     float distance;  

   ...  

   vec3 intersectRay(const vec3 &rayStart, const vec3 &rayEnd)
   {  
     vec3 ray = rayStart - rayEnd; 
 
     float d = vec3.dot(this.normal, ray);  
     float t = signedDistance(rayStart) / d;  

     return (rayStart - t * ray);  
   }  

 }  

And here's the equivalent in TypeScript, using TSM's vec3 class. It uses methods instead of operators for arithmetic operations because JavaScript does not support operator overloading:

class Plane { 
 
   public normal: vec3;  
   public distance: number;
 
   ...  

   intersectRay(rayStart: vec3, rayEnd: vec3): vec3 {  
     var ray = vec3.difference(rayStart, rayEnd);
  
     var d = vec3.dot(this.normal, ray);  
     var t = this.signedDistance(rayStart) / d; 
 
     return vec3.difference(rayStart, ray.copy().scale(t)); 
   }  

 }  

In both versions, three instances of vec3 have are created: the static minus operator as well as vec3.difference both return a new vec3 instance. The same is true for the asterisk operator in the return statement of the C++ version, which scales a vector by a scalar. In the TypeScript version, the non-static method scale is used on a copy of the original vector.

This isn't much of a problem in C++ because the vectors are allocated on the stack; in JavaScript, however, object allocation is expensive and keeps the garbage collector busy. In a simple test scene performing some basic collision detection and frustum culling, the engine created over 400 instances of vec3 per frame! A ten second snapshot of the memory consumption inside Chrome illustrates how frequently the garbage collector had to be invoked:


Ideally, no new objects should be allocated in a static scene. Instead, the results of arithmetic operations should be stored in dedicated variables. To allow for this, I added an optional dest parameter to all methods. If it's not specified, the method creates a new vector instance, just like before. If a valid argument is provided, the result will be written into that instead:

class vec3 { 
 
    static difference(vector: vec3, vector2: vec3, dest: vec3 = null): vec3 {
        if (!dest) dest = new vec3();

        dest.x = vector.x - vector2.x;
        dest.y = vector.y - vector2.y;
        dest.z = vector.z - vector2.z;

        return dest;
    }  

 }  

We can now create a dedicated member variable or a static member variable to permanentely or temporarily hold the result of an operation:

class Plane { 
 
   public normal: vec3;  
   public distance: number;  

   private m_ray = new vec3();  
 
   ...  

   intersectRay(rayStart: vec3, rayEnd: vec3, dest: vec3 = null): vec3 {  
     if (!dest) dest = new vec3(); 
 
     vec3.difference(rayStart, rayEnd, this.m_ray);
  
     var d = vec3.dot(this.normal, this.m_ray);  
     var t = this.signedDistance(rayStart) / d; 
 
     vec3.difference(rayStart, this.m_ray.scale(t), dest); 
 
     return dest;  
   }  

 }  

Using this simple fix in math-heavy routines (frustum culling, collision detection, etc.) I managed to drastically reduce the number of vector instances created each frame.

And although the overall memory consumption is somewhat higher, the garbage collector has to be called less frequently, resulting in a higher, steady framerate:


Applying this fix to other frequently-instantiated classes should increase performance even more. I have updated all of TSM's vector and matrix classes accordingly.

No comments:

Post a Comment