Skip to content

Commit

Permalink
Reimplement Maps and Sets in JS
Browse files Browse the repository at this point in the history
Previously, the only optimized code path for Maps and Sets was for String keys.
This was achieved through an implementation of various complex operations
in Hydrogen. This approach was neither scalable nor forward-compatible.

This patch adds the necessary intrinsics to implement Maps and Sets almost entirely
in JS. The added intrinsics are:

  %_FixedArrayGet
  %_FixedArraySet
  %_TheHole
  %_JSCollectionGetTable
  %_StringGetRawHashField

With these additions, as well as a few changes to what's exposed as runtime functions,
most of the C++ code backing Maps and Sets is gone (including both runtime code in
objects.cc and Crankshaft in hydrogen.cc).

Review URL: https://codereview.chromium.org/947683002

Cr-Commit-Position: refs/heads/master@{#27605}
  • Loading branch information
ajklein authored and Commit bot committed Apr 7, 2015
1 parent 31bbcc3 commit 909500a
Show file tree
Hide file tree
Showing 18 changed files with 351 additions and 1,062 deletions.
10 changes: 0 additions & 10 deletions src/arm/code-stubs-arm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1005,16 +1005,6 @@ void CEntryStub::Generate(MacroAssembler* masm) {

__ VFPEnsureFPSCRState(r2);

// Runtime functions should not return 'the hole'. Allowing it to escape may
// lead to crashes in the IC code later.
if (FLAG_debug_code) {
Label okay;
__ CompareRoot(r0, Heap::kTheHoleValueRootIndex);
__ b(ne, &okay);
__ stop("The hole escaped");
__ bind(&okay);
}

// Check result for exception sentinel.
Label exception_returned;
__ CompareRoot(r0, Heap::kExceptionRootIndex);
Expand Down
236 changes: 209 additions & 27 deletions src/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,86 @@ var $Set = global.Set;
var $Map = global.Map;


// Used by harmony-templates.js
var $MapGet;
var $MapSet;


(function() {


function HashToEntry(table, hash, numBuckets) {
var bucket = ORDERED_HASH_TABLE_HASH_TO_BUCKET(hash, numBuckets);
return ORDERED_HASH_TABLE_BUCKET_AT(table, bucket);
}
%SetInlineBuiltinFlag(HashToEntry);


function SetFindEntry(table, numBuckets, key, hash) {
var keyIsNaN = IS_NUMBER(key) && NUMBER_IS_NAN(key);
for (var entry = HashToEntry(table, hash, numBuckets);
entry !== NOT_FOUND;
entry = ORDERED_HASH_SET_CHAIN_AT(table, entry, numBuckets)) {
var candidate = ORDERED_HASH_SET_KEY_AT(table, entry, numBuckets);
if (key === candidate) {
return entry;
}
if (keyIsNaN && IS_NUMBER(candidate) && NUMBER_IS_NAN(candidate)) {
return entry;
}
}
return NOT_FOUND;
}
%SetInlineBuiltinFlag(SetFindEntry);


function MapFindEntry(table, numBuckets, key, hash) {
var keyIsNaN = IS_NUMBER(key) && NUMBER_IS_NAN(key);
for (var entry = HashToEntry(table, hash, numBuckets);
entry !== NOT_FOUND;
entry = ORDERED_HASH_MAP_CHAIN_AT(table, entry, numBuckets)) {
var candidate = ORDERED_HASH_MAP_KEY_AT(table, entry, numBuckets);
if (key === candidate) {
return entry;
}
if (keyIsNaN && IS_NUMBER(candidate) && NUMBER_IS_NAN(candidate)) {
return entry;
}
}
return NOT_FOUND;
}
%SetInlineBuiltinFlag(MapFindEntry);


function ComputeIntegerHash(key, seed) {
var hash = key;
hash = hash ^ seed;
hash = ~hash + (hash << 15); // hash = (hash << 15) - hash - 1;
hash = hash ^ (hash >>> 12);
hash = hash + (hash << 2);
hash = hash ^ (hash >>> 4);
hash = (hash * 2057) | 0; // hash = (hash + (hash << 3)) + (hash << 11);
hash = hash ^ (hash >>> 16);
return hash;
}
%SetInlineBuiltinFlag(ComputeIntegerHash);


function GetHash(key) {
if (%_IsSmi(key)) {
return ComputeIntegerHash(key, 0);
}
if (IS_STRING(key)) {
var field = %_StringGetRawHashField(key);
if ((field & 1 /* Name::kHashNotComputedMask */) === 0) {
return field >>> 2 /* Name::kHashShift */;
}
}
return %GenericHash(key);
}
%SetInlineBuiltinFlag(GetHash);


// -------------------------------------------------------------------
// Harmony Set

Expand All @@ -35,7 +115,7 @@ function SetConstructor(iterable) {
}


function SetAddJS(key) {
function SetAdd(key) {
if (!IS_SET(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Set.prototype.add', this]);
Expand All @@ -47,34 +127,76 @@ function SetAddJS(key) {
if (key === 0) {
key = 0;
}
return %_SetAdd(this, key);
var table = %_JSCollectionGetTable(this);
var numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
var hash = GetHash(key);
if (SetFindEntry(table, numBuckets, key, hash) !== NOT_FOUND) return this;

var nof = ORDERED_HASH_TABLE_ELEMENT_COUNT(table);
var nod = ORDERED_HASH_TABLE_DELETED_COUNT(table);
var capacity = numBuckets << 1;
if ((nof + nod) >= capacity) {
// Need to grow, bail out to runtime.
%SetGrow(this);
// Re-load state from the grown backing store.
table = %_JSCollectionGetTable(this);
numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
nof = ORDERED_HASH_TABLE_ELEMENT_COUNT(table);
nod = ORDERED_HASH_TABLE_DELETED_COUNT(table);
}
var entry = nof + nod;
var index = ORDERED_HASH_SET_ENTRY_TO_INDEX(entry, numBuckets);
var bucket = ORDERED_HASH_TABLE_HASH_TO_BUCKET(hash, numBuckets);
var chainEntry = ORDERED_HASH_TABLE_BUCKET_AT(table, bucket);
ORDERED_HASH_TABLE_SET_BUCKET_AT(table, bucket, entry);
ORDERED_HASH_TABLE_SET_ELEMENT_COUNT(table, nof + 1);
FIXED_ARRAY_SET(table, index, key);
FIXED_ARRAY_SET_SMI(table, index + 1, chainEntry);
return this;
}


function SetHasJS(key) {
function SetHas(key) {
if (!IS_SET(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Set.prototype.has', this]);
}
return %_SetHas(this, key);
var table = %_JSCollectionGetTable(this);
var numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
var hash = GetHash(key);
return SetFindEntry(table, numBuckets, key, hash) !== NOT_FOUND;
}


function SetDeleteJS(key) {
function SetDelete(key) {
if (!IS_SET(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Set.prototype.delete', this]);
}
return %_SetDelete(this, key);
var table = %_JSCollectionGetTable(this);
var numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
var hash = GetHash(key);
var entry = SetFindEntry(table, numBuckets, key, hash);
if (entry === NOT_FOUND) return false;

var nof = ORDERED_HASH_TABLE_ELEMENT_COUNT(table) - 1;
var nod = ORDERED_HASH_TABLE_DELETED_COUNT(table) + 1;
var index = ORDERED_HASH_SET_ENTRY_TO_INDEX(entry, numBuckets);
FIXED_ARRAY_SET(table, index, %_TheHole());
ORDERED_HASH_TABLE_SET_ELEMENT_COUNT(table, nof);
ORDERED_HASH_TABLE_SET_DELETED_COUNT(table, nod);
if (nof < (numBuckets >>> 1)) %SetShrink(this);
return true;
}


function SetGetSizeJS() {
function SetGetSize() {
if (!IS_SET(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Set.prototype.size', this]);
}
return %_SetGetSize(this);
var table = %_JSCollectionGetTable(this);
return ORDERED_HASH_TABLE_ELEMENT_COUNT(table);
}


Expand Down Expand Up @@ -130,11 +252,11 @@ function SetUpSet() {
%FunctionSetLength(SetForEach, 1);

// Set up the non-enumerable functions on the Set prototype object.
InstallGetter($Set.prototype, "size", SetGetSizeJS);
InstallGetter($Set.prototype, "size", SetGetSize);
InstallFunctions($Set.prototype, DONT_ENUM, $Array(
"add", SetAddJS,
"has", SetHasJS,
"delete", SetDeleteJS,
"add", SetAdd,
"has", SetHas,
"delete", SetDelete,
"clear", SetClearJS,
"forEach", SetForEach
));
Expand Down Expand Up @@ -169,16 +291,21 @@ function MapConstructor(iterable) {
}


function MapGetJS(key) {
function MapGet(key) {
if (!IS_MAP(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Map.prototype.get', this]);
}
return %_MapGet(this, key);
var table = %_JSCollectionGetTable(this);
var numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
var hash = GetHash(key);
var entry = MapFindEntry(table, numBuckets, key, hash);
if (entry === NOT_FOUND) return UNDEFINED;
return ORDERED_HASH_MAP_VALUE_AT(table, entry, numBuckets);
}


function MapSetJS(key, value) {
function MapSet(key, value) {
if (!IS_MAP(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Map.prototype.set', this]);
Expand All @@ -190,34 +317,84 @@ function MapSetJS(key, value) {
if (key === 0) {
key = 0;
}
return %_MapSet(this, key, value);

var table = %_JSCollectionGetTable(this);
var numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
var hash = GetHash(key);
var entry = MapFindEntry(table, numBuckets, key, hash);
if (entry !== NOT_FOUND) {
var existingIndex = ORDERED_HASH_MAP_ENTRY_TO_INDEX(entry, numBuckets);
FIXED_ARRAY_SET(table, existingIndex + 1, value);
return this;
}

var nof = ORDERED_HASH_TABLE_ELEMENT_COUNT(table);
var nod = ORDERED_HASH_TABLE_DELETED_COUNT(table);
var capacity = numBuckets << 1;
if ((nof + nod) >= capacity) {
// Need to grow, bail out to runtime.
%MapGrow(this);
// Re-load state from the grown backing store.
table = %_JSCollectionGetTable(this);
numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
nof = ORDERED_HASH_TABLE_ELEMENT_COUNT(table);
nod = ORDERED_HASH_TABLE_DELETED_COUNT(table);
}
entry = nof + nod;
var index = ORDERED_HASH_MAP_ENTRY_TO_INDEX(entry, numBuckets);
var bucket = ORDERED_HASH_TABLE_HASH_TO_BUCKET(hash, numBuckets);
var chainEntry = ORDERED_HASH_TABLE_BUCKET_AT(table, bucket);
ORDERED_HASH_TABLE_SET_BUCKET_AT(table, bucket, entry);
ORDERED_HASH_TABLE_SET_ELEMENT_COUNT(table, nof + 1);
FIXED_ARRAY_SET(table, index, key);
FIXED_ARRAY_SET(table, index + 1, value);
FIXED_ARRAY_SET(table, index + 2, chainEntry);
return this;
}


function MapHasJS(key) {
function MapHas(key) {
if (!IS_MAP(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Map.prototype.has', this]);
}
return %_MapHas(this, key);
var table = %_JSCollectionGetTable(this);
var numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
var hash = GetHash(key);
return MapFindEntry(table, numBuckets, key, hash) !== NOT_FOUND;
}


function MapDeleteJS(key) {
function MapDelete(key) {
if (!IS_MAP(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Map.prototype.delete', this]);
}
return %_MapDelete(this, key);
var table = %_JSCollectionGetTable(this);
var numBuckets = ORDERED_HASH_TABLE_BUCKET_COUNT(table);
var hash = GetHash(key);
var entry = MapFindEntry(table, numBuckets, key, hash);
if (entry === NOT_FOUND) return false;

var nof = ORDERED_HASH_TABLE_ELEMENT_COUNT(table) - 1;
var nod = ORDERED_HASH_TABLE_DELETED_COUNT(table) + 1;
var index = ORDERED_HASH_MAP_ENTRY_TO_INDEX(entry, numBuckets);
FIXED_ARRAY_SET(table, index, %_TheHole());
FIXED_ARRAY_SET(table, index + 1, %_TheHole());
ORDERED_HASH_TABLE_SET_ELEMENT_COUNT(table, nof);
ORDERED_HASH_TABLE_SET_DELETED_COUNT(table, nod);
if (nof < (numBuckets >>> 1)) %MapShrink(this);
return true;
}


function MapGetSizeJS() {
function MapGetSize() {
if (!IS_MAP(this)) {
throw MakeTypeError('incompatible_method_receiver',
['Map.prototype.size', this]);
}
return %_MapGetSize(this);
var table = %_JSCollectionGetTable(this);
return ORDERED_HASH_TABLE_ELEMENT_COUNT(table);
}


Expand Down Expand Up @@ -271,15 +448,20 @@ function SetUpMap() {
%FunctionSetLength(MapForEach, 1);

// Set up the non-enumerable functions on the Map prototype object.
InstallGetter($Map.prototype, "size", MapGetSizeJS);
InstallGetter($Map.prototype, "size", MapGetSize);
InstallFunctions($Map.prototype, DONT_ENUM, $Array(
"get", MapGetJS,
"set", MapSetJS,
"has", MapHasJS,
"delete", MapDeleteJS,
"get", MapGet,
"set", MapSet,
"has", MapHas,
"delete", MapDelete,
"clear", MapClearJS,
"forEach", MapForEach
));

$MapGet = MapGet;
$MapSet = MapSet;
}

SetUpMap();

})();
Loading

0 comments on commit 909500a

Please sign in to comment.