3

I am using a Proxy object with traps to keep track of object keys so that I can easily iterate over and/or select a random key from the object with little performance overhead. Currently, I am storing the keys in an array as they are added. This is very effective for insertion and random selection, but when a property is deleted, the overhead is massive:

// Benchmark
var testObject = createProxy();

var start = performance.now();

for( var i = 0; i < 1e4; i++ )
  testObject[Math.random() * 1e6 << 0] = true;
for( var i in testObject )
  if( i[0] !== '_' )
    delete testObject[ i ];
    
var end = performance.now();

var total = ( end - start );
console.log( 'Test took ' + total + ' ms' );

// Implementation
function createProxy() {
  function keyTracker() {
    const set = new Set();
    function defineProperty( target, property, descriptor ) {
      target[property] = descriptor.value;
      if( property[0] === '_' ) return true;
      if( set.has( property ) ) return true;
      
      set.add( property );
      target[ '__keys' ].push( property );
      return true;
    }
    function deleteProperty( target, property ) {
      if( property[ 0 ] === '_' ) return true;
      
      delete target[ property ];
      if( !set.delete( property ) ) return true;
      
      target[ '__keys' ] = target[ '__keys' ].filter( 
        key => key !== property
      );
      return true;
    }
    return { defineProperty, deleteProperty };
  }
  
  var proxy = new Proxy(
    Object.defineProperty( {}, '__keys', {
     configurable: true,
     enumerable: false,
     writable: true,
     value: []
  } ), keyTracker() );
  
  return proxy;
}

Calling Array.filter() becomes exponentially expensive as the number of keys in the object grows. I am looking for a solution that is able to allow me to avoid having to call it to remove a single element.

Is there a way I can re-architect this to allow for O(1) insertion, random selection, and deletion of keys?

w0f
  • 908
  • 9
  • 23
  • And you need to care about this... why? User ain't gonna notice if its 10 million ops per sec or 1 million... If you really, *really*, **really** need that much perf you shouldn't be using Proxy(slow)... or Object.defineProperty(slow)... or JavaScript. – Jared Smith Aug 13 '18 at 19:09
  • I understand the performance overhead of Proxy/defineProperty etc. That overhead isn't the problem. The problem is when the amount of keys in the object (being used as a dictionary) grows too large, the performance decreases exponentially because it is currently relying on an array, and ramping up from 10k keys to 1mil keys isn't feasible. This will be running in NodeJS v10, not the browser, and that choice is out of my hands. – w0f Aug 13 '18 at 19:22

2 Answers2

3

You could just use splice. It'll bring you down by 800-1000 ms.

       target[ '__keys' ].splice(target[ '__keys' ].indexOf(property), 1);

// Benchmark
var testObject = createProxy();

var start = performance.now();

for( var i = 0; i < 1e4; i++ )
  testObject[Math.random() * 1e6 << 0] = true;
for( var i in testObject )
  if( i[0] !== '_' )
    delete testObject[ i ];
    
var end = performance.now();

var total = ( end - start );
console.log( 'Test took ' + total + ' ms' );

// Implementation
function createProxy() {
  function keyTracker() {
    const set = new Set();
    function defineProperty( target, property, descriptor ) {
      target[property] = descriptor.value;
      if( property[0] === '_' ) return true;
      if( set.has( property ) ) return true;
      
      set.add( property );
      target[ '__keys' ].push( property );
      return true;
    }
    function deleteProperty( target, property ) {
      if( property[ 0 ] === '_' ) return true;
      
      delete target[ property ];
      if( !set.delete( property ) ) return true;
      
      target[ '__keys' ]
      .splice(target[ '__keys' ].indexOf(property), 1);
      return true;
    }
    return { defineProperty, deleteProperty };
  }
  
  var proxy = new Proxy(
    Object.defineProperty( {}, '__keys', {
     configurable: true,
     enumerable: false,
     writable: true,
     value: []
  } ), keyTracker() );
  
  return proxy;
}
zfrisch
  • 8,474
  • 1
  • 22
  • 34
2

you can use sorted array and then use binary search to achieve O(log n)

// Benchmark
Array.prototype.binarySearch = function (target, comparator) {
    var l = 0,
        h = this.length - 1,
        m, comparison;
    comparator = comparator || function (a, b) {
        return (a < b ? -1 : (a > b ? 1 : 0)); /* default comparison method if one was not provided */
    };
    while (l <= h) {
        m = (l + h) >>> 1; /* equivalent to Math.floor((l + h) / 2) but faster */
        comparison = comparator(this[m], target);
        if (comparison < 0) {
            l = m + 1;
        } else if (comparison > 0) {
            h = m - 1;
        } else {
            return m;
        }
    }
    return~l;
};
Array.prototype.binaryInsert = function (target, duplicate, comparator) {
    var i = this.binarySearch(target, comparator);
    if (i >= 0) { /* if the binarySearch return value was zero or positive, a matching object was found */
        if (!duplicate) {
            return i;
        }
    } else { /* if the return value was negative, the bitwise complement of the return value is the correct index for this object */
        i = ~i;
    }
    this.splice(i, 0, target);
    return i;
};
Array.prototype.binaryDelete = function (target, duplicate, comparator) {
    var i = this.binarySearch(target, comparator);
    if (i >= 0) { /* if the binarySearch return value was zero or positive, a matching object was found */
    this.slice(i,1)
    }
    return i;
};
var testObject = createProxy();

var start = performance.now();

for( var i = 0; i < 1e4; i++ )
  testObject[Math.random() * 1e6 << 0] = true;
for( var i in testObject )
  if( i[0] !== '_' )
    delete testObject[ i ];
    
var end = performance.now();

var total = ( end - start );
console.log( 'Test took ' + total + ' ms' );

// Implementation
function createProxy() {
  function keyTracker() {
    const set = new Set();
    function defineProperty( target, property, descriptor ) {
      target[property] = descriptor.value;
      if( property[0] === '_' ) return true;
      if( set.has( property ) ) return true;
      
      set.add( property );
      target[ '__keys' ].binaryInsert( property );
      return true;
    }
    function deleteProperty( target, property ) {
      if( property[ 0 ] === '_' ) return true;
      
      delete target[ property ];
      if( !set.delete( property ) ) return true;
      
      target[ '__keys' ].binaryDelete(property)
      return true;
    }
    return { defineProperty, deleteProperty };
  }
  
  var proxy = new Proxy(
    Object.defineProperty( {}, '__keys', {
     configurable: true,
     enumerable: false,
     writable: true,
     value: []
  } ), keyTracker() );
  
  return proxy;
}

used the binary search implantation from here javascript-binary-search

Amit Wagner
  • 3,134
  • 3
  • 19
  • 35