Data structures that strongly require to be thread-safe are best candidates to become immutable objects. Moreover, as programmers can write mutable code that behaves in an immutable fashion, how to best achieve immutability? Does this programming approach really affect performance?

Prerequisites

To run the JavaScript presented in the next sections of this blog post, the reader will need to retrieve a copy of ‘immutable.js’ or ‘immutable.min.js’. Few options are available:

  • Installing ‘immutable’ using ‘yarn’.
  • Installing ‘immutable’ using ‘npm’.
  • Downloading the code from a CDN such as CDNJS or jsDelivr

The following is a basic HTML file that can be used to bootstrap all examples (for those who decide to store a local copy of the immutable library):

<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8" />
      <title>Immutable</title>
   </head>
   <body>
      <h1>Immutable</h1>
      <script src="immutable.min.js"></script>
      <script>
         <!-- Examples go here -->
      </script>
   </body>
</html>

Moreover, the following code has to be prepended to all examples as it creates and initialises the array that contains the random test data:

const dataBase = [];
for (let i = 0 ; i < 50; i++) {
   dataBase[i] = [];
   for (let j = 0; j < 50; j++)
      dataBase[i][j] = (Math.random() + 1).toString(36).substring(5);
}

The code above is to generate a two-dimensional array of random strings to emulate a typical key->value relationship.

Scanning mutable and immutable data structures looking for specific data

The following examples investigate how fast mutable and immutable code can scan an entire array looking for specific data. The first section executes a simple loop and prints a message when the array value starts with a number (ad hoc code to measure performance is also added):

/*For cycle performance*/
const t0 = performance.now();

for(let i = 0; i < dataBase[0].length; i++) {
   for(let z = 1; z < dataBase.length; z++) {
      if(z > 0) {
         //Check if string starts with a number 
         if(dataBase[z][i].match(/^\d/))
            console.warn(`For cycle -> ${dataBase[z][i]}`);
      }
   }
}

const t1 = performance.now();

The next block of code tries to achieve the same outcome by using the JavaScript embedded function map():

/*Map() performance*/  
const t0a = performance.now();

const corrArr = dataBase.map((val, index) => {
   return val.map((val1) => {
      //Skip all keys
      if(val1.match(/^\d/) && index > 0)
         console.warn(`Map -> ${val1}`);
       return val1
   });
});

const t1a = performance.now();

The next two bits of code achieve the same result by using immutable code, more in particular the List() and fromJS() methods, in order to spot the differences in performance (if any):

/*Immutable performance with List()*/
const t0b = performance.now();

//Creates an immutable list of arrays
const immArr = Immutable.List(dataBase);
const corrArr1 = immArr.map((itema, index) => {
   return itema.map((itemb) => {
      if(itemb.match(/^\d/) && index > 0)
         console.warn(`Immutable (List) -> ${itemb}`);
      return itema;
   });
});

const t1b = performance.now();

/*Immutable performance with fromJS()*/
const t0c = performance.now();

//Convert everything into Immutable object
const immArr1 = Immutable.fromJS(dataBase);
const corrArr2 = immArr.map((itemc, index) => {
   return itemc.map((itemd) => {
      if(itemd.match(/^\d/) && index > 0)
         console.warn(`Immutable (fromJS) -> ${itemd}`);
      return itemc;
   });
});

const t1c = performance.now();

The following code is again trying to achieve the same result by using a custom method added to the Array base class by using mutable code in an immutable way:

const t0d = performance.now();

/*Immutable wrapper performance*/
Array.prototype.testStr = function() {
   const ar1 = Array.apply([], this);
   for(let i = 0; i < ar1[0].length; i++) {
      for(let z = 1; z < ar1.length; z++) {
         if(z > 0) {
            if(ar1[z][i].match(/^\d/))
               console.warn(`Immutable wrapper -> ${ar1[z][i]}`);
         }
      }
   }
   return ar1;
};            
dataBase.testStr();

const t1d = performance.now(); 

Finally, the following code is to be appended in order to print the execution time of each section:

console.log(`Loop performance: ${t1 - t0} ms`);
console.log(`Map performance: ${t1a - t0a} ms`);
console.log(`Immutable performance (List): ${t1b - t0b} ms`);
console.log(`Immutable performance (fromJS): ${t1c - t0c} ms`);
console.log(`Immutable wrapper performance: ${t1d - t0d} ms`);

While performance will vary according to the software and hardware platform, the reader should not be surprised, should the custom method or the embedded map() run faster than a simple loop cycle. Running the examples with the default provided array (50x50) produced the following output:

Loop performance: 535 ms
Map performance: 558 ms
Immutable performance (List): 499 ms
Immutable performance (fromJS): 609 ms
Immutable wrapper performance: 548 ms

Reloading the page produced the following:

Loop performance: 678 ms
Map performance: 548 ms
Immutable performance (List): 530 ms
Immutable performance (fromJS): 350 ms
Immutable wrapper performance: 549 ms

Increasing the array size to 100x100 produced the following:

Loop performance: 2219 ms
Map performance: 1739 ms
Immutable performance (List): 2107 ms
Immutable performance (fromJS): 1710 ms
Immutable wrapper performance: 2128 ms

While reloading the page with the same settings:

Loop performance: 2216 ms
Map performance: 1604 ms
Immutable performance (List): 2145 ms
Immutable performance (fromJS): 1751 ms
Immutable wrapper performance: 2083 ms

Analysing the results:

  1. The immutable code produced by fromJS() was almost always the fastest solution, way faster than List() which does not generate fully immutable code.
  2. The embedded method map() also ran really fast almost always outperforming the simple loop cycle.
  3. The custom array method was able to outperform the simple loop cycle 50% of the time.
Creating immutable data structures with Immutable.js

The examples above mostly investigated how well immutable and mutable data structures perform when they are searched and parsed in order to look for specific items. This section instead digs into immutable data structures themselves to find out how fast they are generated using the immutable library and how they compare with traditional arrays of objects. The code below generates an array of objects using basic JavaScript and it requires the array ‘dataBase’ whose code was previously shown:

/*List of objects*/
const mark1 = performance.now();            

const dbKey = new Array(dataBase[0].length);                
const dataBaseObjs = new Array(dataBase.length - 1);

for(let i = 1; i < dataBase.length; i++) {
   const dbRow = new Object;
   for(let j = 0, x = 0; j < dataBase[0].length; j++, x++) {
      dbKey[j] = dataBase[0][x];
      dbRow[dbKey[j]] = dataBase[i][j];
   }
   dataBaseObjs[i - 1] = dbRow;
}

const mark2 = performance.now();

Next, the Immutable.js code required to create an immutable array follows:

/*Immutable*/
const mark3 = performance.now();

const tempKeys = Immutable.List(dataBase[0]);
const tempData = Immutable.List(dataBase);
const tempMap = tempData.map((val, index) => {
   if(!index) return;
   return Immutable.Map(tempKeys.zip(val));
});
const myMap = tempMap.shift();

const mark4 = performance.now();

Finally, the following code is to be appended in order to print the execution time of both section:

console.log(`List of objects performance: ${mark2 - mark1} ms`);
console.log(`Immutable performance: ${mark4 - mark3} ms`);

Running the code twice with a 50x50 array produce the following timing:

List of objects performance: 2 ms
Immutable performance: 123 ms

List of objects performance: 2 ms
Immutable performance: 148 ms

While increasing the size to a 100x100 array:

List of objects performance: 8 ms
Immutable performance: 189 ms

List of objects performance: 7 ms
Immutable performance: 178 ms

Even though the code built using the Immutable.js library seems to perform increasingly better as the number of items increases, these results show immutable objects take longer to be created than traditional arrays of objects.

Conclusions

While Immutable.js can help dealing with issues such as thread-safety, they might not perform as expected. In fact, the example above showed they were quick at scanning data structures but slow at creating new objects. As usual, programmers should carefully carefully test carefully their code on the target platform as their timings might not match the ones presented here.

Previous Post Next Post