Commit 007eac94 authored by cbruni's avatar cbruni Committed by Commit bot

Improve JSReceiver::GetKeys Speed

The core bottleneck lies in N-square cost of array union. Depending on the size
of the arrays involved it makes sense to rely on a hash-set/table for the lookup.

LOG=N
BUG=v8:2904

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

Cr-Commit-Position: refs/heads/master@{#30797}
parent 7af79ae6
......@@ -97,18 +97,6 @@ ELEMENTS_LIST(ELEMENTS_TRAITS)
#undef ELEMENTS_TRAITS
static bool HasIndex(Handle<FixedArray> array, Handle<Object> index_handle) {
DisallowHeapAllocation no_gc;
Object* index = *index_handle;
int len0 = array->length();
for (int i = 0; i < len0; i++) {
Object* element = array->get(i);
if (index->KeyEquals(element)) return true;
}
return false;
}
MUST_USE_RESULT
MaybeHandle<Object> ThrowArrayLengthRangeError(Isolate* isolate) {
THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kInvalidArrayLength),
......@@ -885,78 +873,26 @@ class ElementsAccessorBase : public ElementsAccessor {
from, from_start, *to, from_kind, to_start, packed_size, copy_size);
}
virtual Handle<FixedArray> AddElementsToFixedArray(
Handle<JSObject> receiver, Handle<FixedArray> to,
virtual void AddElementsToKeyAccumulator(Handle<JSObject> receiver,
KeyAccumulator* accumulator,
FixedArray::KeyFilter filter) final {
Handle<FixedArrayBase> from(receiver->elements());
int len0 = to->length();
#ifdef ENABLE_SLOW_DCHECKS
if (FLAG_enable_slow_asserts) {
for (int i = 0; i < len0; i++) {
DCHECK(!to->get(i)->IsTheHole());
}
}
#endif
// Optimize if 'other' is empty.
// We cannot optimize if 'this' is empty, as other may have holes.
uint32_t len1 = ElementsAccessorSubclass::GetCapacityImpl(*receiver, *from);
if (len1 == 0) return to;
Isolate* isolate = from->GetIsolate();
// Compute how many elements are not in other.
uint32_t extra = 0;
for (uint32_t y = 0; y < len1; y++) {
if (ElementsAccessorSubclass::HasEntryImpl(*from, y)) {
Handle<Object> value = ElementsAccessorSubclass::GetImpl(from, y);
uint32_t add_length =
ElementsAccessorSubclass::GetCapacityImpl(*receiver, *from);
if (add_length == 0) return;
accumulator->PrepareForComparisons(add_length);
int prev_key_count = accumulator->GetLength();
for (uint32_t i = 0; i < add_length; i++) {
if (!ElementsAccessorSubclass::HasEntryImpl(*from, i)) continue;
Handle<Object> value = ElementsAccessorSubclass::GetImpl(from, i);
DCHECK(!value->IsTheHole());
DCHECK(!value->IsAccessorPair());
DCHECK(!value->IsExecutableAccessorInfo());
if (filter == FixedArray::NON_SYMBOL_KEYS && value->IsSymbol()) {
continue;
}
if (!HasIndex(to, value)) {
extra++;
}
}
accumulator->AddKey(value, prev_key_count);
}
if (extra == 0) return to;
// Allocate the result
Handle<FixedArray> result = isolate->factory()->NewFixedArray(len0 + extra);
// Fill in the content
{
DisallowHeapAllocation no_gc;
WriteBarrierMode mode = result->GetWriteBarrierMode(no_gc);
for (int i = 0; i < len0; i++) {
Object* e = to->get(i);
DCHECK(e->IsString() || e->IsNumber());
result->set(i, e, mode);
}
}
// Fill in the extra values.
uint32_t entry = 0;
for (uint32_t y = 0; y < len1; y++) {
if (ElementsAccessorSubclass::HasEntryImpl(*from, y)) {
Handle<Object> value = ElementsAccessorSubclass::GetImpl(from, y);
DCHECK(!value->IsAccessorPair());
DCHECK(!value->IsExecutableAccessorInfo());
if (filter == FixedArray::NON_SYMBOL_KEYS && value->IsSymbol()) {
continue;
}
if (!value->IsTheHole() && !HasIndex(to, value)) {
result->set(len0 + entry, *value);
entry++;
}
}
}
DCHECK(extra == entry);
return result;
}
static uint32_t GetCapacityImpl(JSObject* holder,
......
......@@ -100,8 +100,8 @@ class ElementsAccessor {
virtual void GrowCapacityAndConvert(Handle<JSObject> object,
uint32_t capacity) = 0;
virtual Handle<FixedArray> AddElementsToFixedArray(
Handle<JSObject> receiver, Handle<FixedArray> to,
virtual void AddElementsToKeyAccumulator(Handle<JSObject> receiver,
KeyAccumulator* accumulator,
FixedArray::KeyFilter filter) = 0;
// Returns a shared ElementsAccessor for the specified ElementsKind.
......
......@@ -6590,11 +6590,123 @@ Handle<FixedArray> JSObject::GetEnumPropertyKeys(Handle<JSObject> object,
}
Handle<FixedArray> KeyAccumulator::GetKeys() {
if (length_ == 0) {
return isolate_->factory()->empty_fixed_array();
}
if (set_.is_null()) {
keys_->Shrink(length_);
return keys_;
}
// copy over results from set_
Handle<FixedArray> result = isolate_->factory()->NewFixedArray(length_);
for (int i = 0; i < length_; i++) {
result->set(i, set_->KeyAt(i));
}
return result;
}
void KeyAccumulator::AddKey(Handle<Object> key, int check_limit) {
#ifdef ENABLE_SLOW_DCHECKS
if (FLAG_enable_slow_asserts) {
DCHECK(key->IsNumber() || key->IsName());
}
#endif
if (!set_.is_null()) {
set_ = OrderedHashSet::Add(set_, key);
length_ = set_->NumberOfElements();
return;
}
// check if we already have the key in the case we are still using
// the keys_ FixedArray
check_limit = Min(check_limit, length_);
for (int i = 0; i < check_limit; i++) {
Object* current = keys_->get(i);
if (current->KeyEquals(*key)) return;
}
EnsureCapacity(length_);
keys_->set(length_, *key);
length_++;
}
void KeyAccumulator::AddKeys(Handle<FixedArray> array,
FixedArray::KeyFilter filter) {
int add_length = array->length();
if (add_length == 0) return;
if (keys_.is_null() && filter == FixedArray::ALL_KEYS) {
keys_ = array;
length_ = keys_->length();
return;
}
PrepareForComparisons(add_length);
int previous_key_count = length_;
for (int i = 0; i < add_length; i++) {
Handle<Object> current(array->get(i), isolate_);
if (filter == FixedArray::NON_SYMBOL_KEYS && current->IsSymbol()) continue;
AddKey(current, previous_key_count);
}
}
void KeyAccumulator::AddKeys(Handle<JSObject> array_like,
FixedArray::KeyFilter filter) {
DCHECK(array_like->IsJSArray() || array_like->HasSloppyArgumentsElements());
ElementsAccessor* accessor = array_like->GetElementsAccessor();
accessor->AddElementsToKeyAccumulator(array_like, this, filter);
}
void KeyAccumulator::PrepareForComparisons(int count) {
// Depending on how many comparisons we do we should switch to the
// hash-table-based checks which have a one-time overhead for
// initializing but O(1) for HasKey checks.
if (!set_.is_null()) return;
// This limit was obtained through evaluation of a microbench.
if (length_ * count < 50) return;
set_ = OrderedHashSet::Allocate(isolate_, length_);
for (int i = 0; i < length_; i++) {
Handle<Object> value(keys_->get(i), isolate_);
set_ = OrderedHashSet::Add(set_, value);
}
}
void KeyAccumulator::EnsureCapacity(int capacity) {
if (keys_.is_null() || keys_->length() <= capacity) {
Grow();
}
}
void KeyAccumulator::Grow() {
// The OrderedHashSet handles growing by itself.
if (!set_.is_null()) return;
// Otherwise, grow the internal keys_ FixedArray
int capacity = keys_.is_null() ? 16 : keys_->length() * 2 + 16;
Handle<FixedArray> new_keys = isolate_->factory()->NewFixedArray(capacity);
if (keys_.is_null()) {
keys_ = new_keys;
return;
}
int buffer_length = keys_->length();
{
DisallowHeapAllocation no_gc;
WriteBarrierMode mode = new_keys->GetWriteBarrierMode(no_gc);
for (int i = 0; i < buffer_length; i++) {
new_keys->set(i, keys_->get(i), mode);
}
}
keys_ = new_keys;
}
MaybeHandle<FixedArray> JSReceiver::GetKeys(Handle<JSReceiver> object,
KeyCollectionType type) {
USE(ContainsOnlyValidKeys);
Isolate* isolate = object->GetIsolate();
Handle<FixedArray> content = isolate->factory()->empty_fixed_array();
KeyAccumulator accumulator(isolate);
Handle<JSFunction> arguments_function(
JSFunction::cast(isolate->sloppy_arguments_map()->GetConstructor()));
......@@ -6617,11 +6729,7 @@ MaybeHandle<FixedArray> JSReceiver::GetKeys(Handle<JSReceiver> object,
arraysize(args),
args),
FixedArray);
ASSIGN_RETURN_ON_EXCEPTION(
isolate, content,
FixedArray::AddKeysFromArrayLike(
content, Handle<JSObject>::cast(names)),
FixedArray);
accumulator.AddKeys(Handle<JSObject>::cast(names), FixedArray::ALL_KEYS);
break;
}
......@@ -6640,23 +6748,17 @@ MaybeHandle<FixedArray> JSReceiver::GetKeys(Handle<JSReceiver> object,
Handle<FixedArray> element_keys =
isolate->factory()->NewFixedArray(current->NumberOfEnumElements());
current->GetEnumElementKeys(*element_keys);
ASSIGN_RETURN_ON_EXCEPTION(
isolate, content,
FixedArray::UnionOfKeys(content, element_keys),
FixedArray);
DCHECK(ContainsOnlyValidKeys(content));
accumulator.AddKeys(element_keys, FixedArray::ALL_KEYS);
DCHECK(ContainsOnlyValidKeys(accumulator.GetKeys()));
// Add the element keys from the interceptor.
if (current->HasIndexedInterceptor()) {
Handle<JSObject> result;
if (JSObject::GetKeysForIndexedInterceptor(
current, object).ToHandle(&result)) {
ASSIGN_RETURN_ON_EXCEPTION(
isolate, content,
FixedArray::AddKeysFromArrayLike(content, result),
FixedArray);
accumulator.AddKeys(result, FixedArray::ALL_KEYS);
}
DCHECK(ContainsOnlyValidKeys(content));
DCHECK(ContainsOnlyValidKeys(accumulator.GetKeys()));
}
// We can cache the computed property keys if access checks are
......@@ -6674,27 +6776,26 @@ MaybeHandle<FixedArray> JSReceiver::GetKeys(Handle<JSReceiver> object,
!current->IsJSValue() && !current->IsAccessCheckNeeded() &&
!current->HasNamedInterceptor() && !current->HasIndexedInterceptor());
// Compute the property keys and cache them if possible.
ASSIGN_RETURN_ON_EXCEPTION(
isolate, content,
FixedArray::UnionOfKeys(
content, JSObject::GetEnumPropertyKeys(current, cache_enum_keys)),
FixedArray);
DCHECK(ContainsOnlyValidKeys(content));
Handle<FixedArray> enum_keys =
JSObject::GetEnumPropertyKeys(current, cache_enum_keys);
accumulator.AddKeys(enum_keys, FixedArray::ALL_KEYS);
DCHECK(ContainsOnlyValidKeys(accumulator.GetKeys()));
// Add the non-symbol property keys from the interceptor.
if (current->HasNamedInterceptor()) {
Handle<JSObject> result;
if (JSObject::GetKeysForNamedInterceptor(
current, object).ToHandle(&result)) {
ASSIGN_RETURN_ON_EXCEPTION(
isolate, content, FixedArray::AddKeysFromArrayLike(
content, result, FixedArray::NON_SYMBOL_KEYS),
FixedArray);
accumulator.AddKeys(result, FixedArray::NON_SYMBOL_KEYS);
}
DCHECK(ContainsOnlyValidKeys(content));
DCHECK(ContainsOnlyValidKeys(accumulator.GetKeys()));
}
}
return content;
Handle<FixedArray> keys = accumulator.GetKeys();
DCHECK(ContainsOnlyValidKeys(keys));
return keys;
}
......@@ -8195,53 +8296,6 @@ void FixedArray::Shrink(int new_length) {
}
MaybeHandle<FixedArray> FixedArray::AddKeysFromArrayLike(
Handle<FixedArray> content, Handle<JSObject> array, KeyFilter filter) {
DCHECK(array->IsJSArray() || array->HasSloppyArgumentsElements());
ElementsAccessor* accessor = array->GetElementsAccessor();
Handle<FixedArray> result =
accessor->AddElementsToFixedArray(array, content, filter);
#ifdef ENABLE_SLOW_DCHECKS
if (FLAG_enable_slow_asserts) {
DisallowHeapAllocation no_allocation;
for (int i = 0; i < result->length(); i++) {
Object* current = result->get(i);
DCHECK(current->IsNumber() || current->IsName());
}
}
#endif
return result;
}
MaybeHandle<FixedArray> FixedArray::UnionOfKeys(Handle<FixedArray> first,
Handle<FixedArray> second) {
if (second->length() == 0) return first;
if (first->length() == 0) return second;
Isolate* isolate = first->GetIsolate();
Handle<FixedArray> result =
isolate->factory()->NewFixedArray(first->length() + second->length());
for (int i = 0; i < first->length(); i++) {
result->set(i, first->get(i));
}
int pos = first->length();
for (int j = 0; j < second->length(); j++) {
Object* current = second->get(j);
int i;
for (i = 0; i < first->length(); i++) {
if (current->KeyEquals(first->get(i))) break;
}
if (i == first->length()) {
result->set(pos++, current);
}
}
result->Shrink(pos);
return result;
}
void FixedArray::CopyTo(int pos, FixedArray* dest, int dest_pos, int len) {
DisallowHeapAllocation no_gc;
WriteBarrierMode mode = dest->GetWriteBarrierMode(no_gc);
......@@ -15564,6 +15618,48 @@ Handle<Derived> OrderedHashTable<Derived, Iterator, entrysize>::Clear(
return new_table;
}
template <class Derived, class Iterator, int entrysize>
bool OrderedHashTable<Derived, Iterator, entrysize>::HasKey(
Handle<Derived> table, Handle<Object> key) {
int entry = table->KeyToFirstEntry(*key);
// Walk the chain in the bucket to find the key.
while (entry != kNotFound) {
Object* candidate_key = table->KeyAt(entry);
if (candidate_key->SameValueZero(*key)) return true;
entry = table->NextChainEntry(entry);
}
return false;
}
Handle<OrderedHashSet> OrderedHashSet::Add(Handle<OrderedHashSet> table,
Handle<Object> key) {
int hash = Object::GetOrCreateHash(table->GetIsolate(), key)->value();
int entry = table->HashToEntry(hash);
// Walk the chain of the bucket and try finding the key.
while (entry != kNotFound) {
Object* candidate_key = table->KeyAt(entry);
// Do not add if we have the key already
if (candidate_key->SameValueZero(*key)) return table;
entry = table->NextChainEntry(entry);
}
table = OrderedHashSet::EnsureGrowable(table);
// Read the existing bucket values.
int bucket = table->HashToBucket(hash);
int previous_entry = table->HashToEntry(hash);
int nof = table->NumberOfElements();
// Insert a new entry at the end,
int new_entry = nof + table->NumberOfDeletedElements();
int new_index = table->EntryToIndex(new_entry);
table->set(new_index, *key);
table->set(new_index + kChainOffset, Smi::FromInt(previous_entry));
// and point the bucket to the new entry.
table->set(kHashTableStartIndex + bucket, Smi::FromInt(new_entry));
table->SetNumberOfElements(nof + 1);
return table;
}
template<class Derived, class Iterator, int entrysize>
Handle<Derived> OrderedHashTable<Derived, Iterator, entrysize>::Rehash(
......@@ -15626,6 +15722,9 @@ template Handle<OrderedHashSet>
OrderedHashTable<OrderedHashSet, JSSetIterator, 1>::Clear(
Handle<OrderedHashSet> table);
template bool OrderedHashTable<OrderedHashSet, JSSetIterator, 1>::HasKey(
Handle<OrderedHashSet> table, Handle<Object> key);
template Handle<OrderedHashMap>
OrderedHashTable<OrderedHashMap, JSMapIterator, 2>::Allocate(
......@@ -15643,6 +15742,9 @@ template Handle<OrderedHashMap>
OrderedHashTable<OrderedHashMap, JSMapIterator, 2>::Clear(
Handle<OrderedHashMap> table);
template bool OrderedHashTable<OrderedHashMap, JSMapIterator, 2>::HasKey(
Handle<OrderedHashMap> table, Handle<Object> key);
template<class Derived, class TableType>
void OrderedHashTableIterator<Derived, TableType>::Transition() {
......
......@@ -2472,17 +2472,6 @@ class FixedArray: public FixedArrayBase {
enum KeyFilter { ALL_KEYS, NON_SYMBOL_KEYS };
// Add the elements of a JSArray to this FixedArray.
MUST_USE_RESULT static MaybeHandle<FixedArray> AddKeysFromArrayLike(
Handle<FixedArray> content, Handle<JSObject> array,
KeyFilter filter = ALL_KEYS);
// Computes the union of keys and return the result.
// Used for implementing "for (n in object) { }"
MUST_USE_RESULT static MaybeHandle<FixedArray> UnionOfKeys(
Handle<FixedArray> first,
Handle<FixedArray> second);
// Copy a sub array from the receiver to dest.
void CopyTo(int pos, FixedArray* dest, int dest_pos, int len);
......@@ -3661,6 +3650,9 @@ class OrderedHashTable: public FixedArray {
// exisiting iterators can be updated.
static Handle<Derived> Clear(Handle<Derived> table);
// Returns a true if the OrderedHashTable contains the key
static bool HasKey(Handle<Derived> table, Handle<Object> key);
int NumberOfElements() {
return Smi::cast(get(kNumberOfElementsIndex))->value();
}
......@@ -3680,6 +3672,26 @@ class OrderedHashTable: public FixedArray {
return kHashTableStartIndex + NumberOfBuckets() + (entry * kEntrySize);
}
int HashToBucket(int hash) { return hash & (NumberOfBuckets() - 1); }
int HashToEntry(int hash) {
int bucket = HashToBucket(hash);
Object* entry = this->get(kHashTableStartIndex + bucket);
return Smi::cast(entry)->value();
}
int KeyToFirstEntry(Object* key) {
Object* hash = key->GetHash();
// If the object does not have an identity hash, it was never used as a key
if (hash->IsUndefined()) return kNotFound;
return HashToEntry(Smi::cast(hash)->value());
}
int NextChainEntry(int entry) {
Object* next_entry = get(EntryToIndex(entry) + kChainOffset);
return Smi::cast(next_entry)->value();
}
Object* KeyAt(int entry) { return get(EntryToIndex(entry)); }
bool IsObsolete() {
......@@ -3726,7 +3738,7 @@ class OrderedHashTable: public FixedArray {
// optimize that case.
static const int kClearedTableSentinel = -1;
private:
protected:
static Handle<Derived> Rehash(Handle<Derived> table, int new_capacity);
void SetNumberOfBuckets(int num) {
......@@ -3768,6 +3780,9 @@ class OrderedHashSet: public OrderedHashTable<
OrderedHashSet, JSSetIterator, 1> {
public:
DECLARE_CAST(OrderedHashSet)
static Handle<OrderedHashSet> Add(Handle<OrderedHashSet> table,
Handle<Object> value);
};
......@@ -10488,6 +10503,29 @@ class BooleanBit : public AllStatic {
}
};
class KeyAccumulator final BASE_EMBEDDED {
public:
explicit KeyAccumulator(Isolate* isolate) : isolate_(isolate), length_(0) {}
void AddKey(Handle<Object> key, int check_limit);
void AddKeys(Handle<FixedArray> array, FixedArray::KeyFilter filter);
void AddKeys(Handle<JSObject> array, FixedArray::KeyFilter filter);
void PrepareForComparisons(int count);
Handle<FixedArray> GetKeys();
int GetLength() { return length_; }
private:
void EnsureCapacity(int capacity);
void Grow();
Isolate* isolate_;
Handle<FixedArray> keys_;
Handle<OrderedHashSet> set_;
int length_;
DISALLOW_COPY_AND_ASSIGN(KeyAccumulator);
};
} } // namespace v8::internal
#endif // V8_OBJECTS_H_
......@@ -197,8 +197,15 @@ RUNTIME_FUNCTION(Runtime_GetArrayKeys) {
DCHECK(args.length() == 2);
CONVERT_ARG_HANDLE_CHECKED(JSObject, array, 0);
CONVERT_NUMBER_CHECKED(uint32_t, length, Uint32, args[1]);
if (array->elements()->IsDictionary()) {
Handle<FixedArray> keys = isolate->factory()->empty_fixed_array();
if (!array->elements()->IsDictionary()) {
RUNTIME_ASSERT(array->HasFastSmiOrObjectElements() ||
array->HasFastDoubleElements());
uint32_t actual_length = static_cast<uint32_t>(array->elements()->length());
return *isolate->factory()->NewNumberFromUint(Min(actual_length, length));
}
KeyAccumulator accumulator(isolate);
for (PrototypeIterator iter(isolate, array,
PrototypeIterator::START_AT_RECEIVER);
!iter.IsAtEnd(); iter.Advance()) {
......@@ -213,22 +220,16 @@ RUNTIME_FUNCTION(Runtime_GetArrayKeys) {
Handle<FixedArray> current_keys =
isolate->factory()->NewFixedArray(current->NumberOfOwnElements(NONE));
current->GetOwnElementKeys(*current_keys, NONE);
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, keys, FixedArray::UnionOfKeys(keys, current_keys));
accumulator.AddKeys(current_keys, FixedArray::ALL_KEYS);
}
// Erase any keys >= length.
// TODO(adamk): Remove this step when the contract of %GetArrayKeys
// is changed to let this happen on the JS side.
Handle<FixedArray> keys = accumulator.GetKeys();
for (int i = 0; i < keys->length(); i++) {
if (NumberToUint32(keys->get(i)) >= length) keys->set_undefined(i);
}
return *isolate->factory()->NewJSArrayWithElements(keys);
} else {
RUNTIME_ASSERT(array->HasFastSmiOrObjectElements() ||
array->HasFastDoubleElements());
uint32_t actual_length = static_cast<uint32_t>(array->elements()->length());
return *isolate->factory()->NewNumberFromUint(Min(actual_length, length));
}
}
......
......@@ -996,19 +996,26 @@ RUNTIME_FUNCTION(Runtime_OwnKeys) {
// a fresh clone on each invocation.
int length = contents->length();
Handle<FixedArray> copy = isolate->factory()->NewFixedArray(length);
for (int i = 0; i < length; i++) {
int offset = 0;
// Use an outer loop to avoid creating too many handles in the current
// handle scope.
while (offset < length) {
HandleScope scope(isolate);
offset += 100;
int end = Min(offset, length);
for (int i = offset - 100; i < end; i++) {
Object* entry = contents->get(i);
if (entry->IsString()) {
copy->set(i, entry);
} else {
DCHECK(entry->IsNumber());
HandleScope scope(isolate);
Handle<Object> entry_handle(entry, isolate);
Handle<Object> entry_str =
isolate->factory()->NumberToString(entry_handle);
copy->set(i, *entry_str);
}
}
}
return *isolate->factory()->NewJSArrayWithElements(copy);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment