/* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */ /* vi: set ts=4 sw=4 expandtab: (add to ~/.vimrc: set modeline modelines=5) */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "avmplus.h" using namespace MMgc; // WARNING: // WARNING: // WARNING: // Never do "MMgc::GC::GetGC(this)" in the HashTable object. It is dynamically // placed at the end of a ScriptObject following traits data which can extend // greater than 4k off the starting pointer. This will cause GetGC(this) to fail. // WARNING: // WARNING: // WARNING: namespace avmplus { void InlineHashtable::initialize(GC *gc, int capacity) { MMGC_STATIC_ASSERT(sizeof(InlineHashtable) == (sizeof(void*) * 2)); // isEmpty() requires this, so let's insure it... MMGC_STATIC_ASSERT(EMPTY == 0); capacity = MathUtils::nextPowerOfTwo(capacity); setCapacity(capacity*2); AvmAssert(getCapacity()); MMGC_MEM_TYPE(gc->FindBeginningGuarded(this)); uint32_t const cap = getCapacity() + (hasIterIndex() ? 2 : 0); setAtoms(createAtoms(gc, cap)); } void InlineHashtable::destroy() { Atom* atoms = getAtoms(); if(atoms) { #if defined(VMCFG_TELEMETRY_SAMPLER) && defined(DEBUGGER) if (getAtomContainer()) { GC* gc = GC::GetGC(getAtomContainer()); if (gc && gc->GetAttachedSampler()) { ((IMemorySampler*)gc->GetAttachedSampler())->recordDeallocation(this, 0); } } #endif // deliberately ignore the final two entries if hasIterIndex(), since they aren't Atoms AvmCore::decrementAtomRegion(atoms, getCapacity()); freeAtoms(); } m_atomsAndFlags = 0; m_size = 0; m_logCapacity = 0; } void InlineHashtable::setAtoms(InlineHashtable::AtomContainer* newAtoms) { GC *gc = GC::GetGC(newAtoms); uintptr_t newVal = uintptr_t(newAtoms) | (m_atomsAndFlags & kAtomFlags); WB(gc, gc->FindBeginningFast(this), &m_atomsAndFlags, (void*)newVal); } // Return true if the table slot was previously empty, i.e., we have added a // new entry. Return false if we've simply updated the value of an existing entry. // If a new entry is added, the caller is responsible expanding the table to // maintain at least one free slot, and to prune any weak entries that are dead. bool InlineHashtable::put(Atom name, Atom value) { AvmAssert(name != EMPTY && value != EMPTY); AtomContainer* atomContainer = getAtomContainer(); Atom* atoms = atomContainer->atoms; int i = find(name, atoms, getCapacity()); GC *gc = GC::GetGC(atoms); if (removeDontEnumMask(atoms[i]) != name) { // Create new entry. AvmAssert(!isFull()); //atoms[i] = name; WBATOM(gc, atomContainer, &atoms[i], name); m_size++; //atoms[i+1] = value; WBATOM(gc, atomContainer, &atoms[i+1], value); return true; } else { // Update existing entry. //atoms[i+1] = value; WBATOM(gc, atomContainer, &atoms[i+1], value); return false; } } Atom InlineHashtable::get(Atom name) const { int i; const Atom* atoms = getAtoms(); return atoms[i = find(name, atoms, getCapacity())] == name ? atoms[i+1] : undefinedAtom; } int InlineHashtable::find(Stringp x, const Atom *t, uint32_t tLen) const { AvmAssert(x != NULL); return find(x->atom(), t, tLen); } int InlineHashtable::find(Atom x, const Atom *t, uint32_t m) const { uintptr_t mask = ~(m_atomsAndFlags & kDontEnumBit); x &= mask; #if 0 // debug code to print out the strings we're searching for static int debug =0; if (debug && AvmCore::isString(x)) { Stringp s = AvmCore::atomToString(x); AvmDebugMsg (s->c_str(), false); AvmDebugMsg ("\n", false); } #endif uintptr_t bitmask = (m - 1) & ~0x1; AvmAssert(x != EMPTY && x != DELETED); // this is a quadratic probe but we only hit even numbered slots since those hold keys. int n = 7 << 1; #ifdef _DEBUG uint32_t loopCount = 0; #endif // Note: Mask off MSB to avoid negative indices. Mask off bottom // 3 bits because it doesn't contribute to hash. Double it // because names, values stored adjacently. uint32_t i = ((0x7FFFFFF8 & x)>>2) & bitmask; Atom k; while ((k=t[i]&mask) != x && k != EMPTY) { i = (i + (n += 2)) & bitmask; // quadratic probe AvmAssert(loopCount++ < m); // don't scan forever } AvmAssert(i <= ((m-1)&~0x1)); return i; } void InlineHashtable::deletePairAt(int i) { Atom* atoms = getAtoms(); // decrement refcount as necessary. AvmCore::atomWriteBarrier_dtor(&atoms[i]); AvmCore::atomWriteBarrier_dtor(&atoms[i+1]); // calls above set the slot to 0, we want DELETED atoms[i] = DELETED; atoms[i+1] = DELETED; setHasDeletedItems(); } Atom InlineHashtable::remove(Atom name) { Atom* atoms = getAtoms(); int i = find(name, atoms, getCapacity()); Atom val = undefinedAtom; if (removeDontEnumMask(atoms[i]) == name) { val = atoms[i+1]; deletePairAt(i); } return val; } int InlineHashtable::rehash(const Atom *oldAtoms, int oldlen, Atom *newAtoms, int newlen) const { int newSize = 0; for (int i=0, n=oldlen; i < n; i += 2) { Atom oldAtom; if ((oldAtom=oldAtoms[i]) != EMPTY && oldAtom != DELETED) { // inlined & simplified version of put() int j = find(oldAtom, newAtoms, newlen); newAtoms[j] = oldAtom; newAtoms[j+1] = oldAtoms[i+1]; newSize++; } } return newSize; } /** * load factor is 0.75 so we're full if size >= M*0.75 * where M = atoms.length/2
* size >= M*3/4
* 4*size >= 3*M
* 4*size >= 3*atoms.length/2
* 8*size >= 3*atoms.length
* size<<3 >= 3*atoms.length */ /** * load factor is 0.9 so we're full if size >= M*0.9 * where M = atoms.length/2
* size >= M*9/10
* 10*size >= 9*M
* 10*size >= 9*atoms.length/2
* 20*size >= 9*atoms.length
*/ bool InlineHashtable::isFull() const { // This seems to work very well and the Intel compiler converts both the multiplies // into shift+add operations. return uint32_t((5*(m_size+1))) >= uint32_t(2*getCapacity()); // 0.80 #if 0 //return ((size<<3)+1) >= 3*getCapacity(); // 0.75 //return ((40*size)+1) >= 19*getCapacity(); // 0.95 //return ((40*size)+1) >= 18*getCapacity(); // 0.90 //return ((40*size)+1) >= 17*getCapacity(); // 0.85 //return ((40*size)+1) >= 15*getCapacity(); // 0.75 //Edwins suggestion - if (size > max - max>>N)) grow (N = 5, 4, 3, 2) //#define SHIFTFACTOR 4 //NOT CORRECT //return ( ( size << SHIFTFACTOR ) + size > (getCapacity() << (SHIFTFACTOR-1)) ); #endif } void InlineHashtable::add(Atom name, Atom value, Toplevel* toplevel) { if (put(name, value)) { if (isFull()) { grow(toplevel); } } } void InlineHashtable::grow(Toplevel* toplevel) { // grow the table by 2N+1 // new = 2*old+1 ; old == o.atoms.length/2 // = 2*(o.atoms.length/2)+1 // = o.atoms.length + 1 // If we have deleted slots, we don't grow our HT because our rehash will clear // out spots for us. const uint32_t oldCapacity = getCapacity(); const uint32_t newCapacity = hasDeletedItems() ? oldCapacity : MathUtils::nextPowerOfTwo(oldCapacity+1); if (newCapacity > MAX_CAPACITY) { if (toplevel != NULL) toplevel->throwError(kOutOfMemoryError); else GCHeap::SignalObjectTooLarge(); } const Atom* atoms = getAtoms(); GC* gc = GC::GetGC(atoms); MMGC_MEM_TYPE(gc->FindBeginningGuarded(this)); const uint32_t newCapacityAlloc = newCapacity + (hasIterIndex() ? 2 : 0); AtomContainer* atomContainer = createAtoms(gc, newCapacityAlloc); Atom* newAtoms = atomContainer->atoms; // WE#2621977 -- the behavior of growing-during-enumeration has always been ill-defined, // but this change caused that to (effectively) terminate the enumeration. This is a hack: // if we have an iter index, copy it over to preserve the previous oddball behavior. (srj) if (m_atomsAndFlags & kHasIterIndex) { intptr_t* oldIterIndices = (intptr_t*)(atoms + oldCapacity); intptr_t* newIterIndices = (intptr_t*)(newAtoms + newCapacity); newIterIndices[0] = oldIterIndices[0]; newIterIndices[1] = oldIterIndices[1]; } m_size = rehash(atoms, oldCapacity, newAtoms, newCapacity); freeAtoms(); setAtoms(atomContainer); setCapacity(newCapacity); clrHasDeletedItems(); #if defined(VMCFG_TELEMETRY_SAMPLER) && defined(DEBUGGER) if (gc->GetAttachedSampler()) { ((IMemorySampler *)gc->GetAttachedSampler())->recordObjectReallocation(this); } #endif } // this function is slow -- however, for "normal", well-mannered ABC code, it should // be called only once per iteration, at the start. (For handcrafted ABC it could be // called repeatedly, which would dramatically slow such cases, but that's acceptable.) int InlineHashtable::publicIterIndexToRealIndex(int publicIndex) { // keep in mind that a "public" index goes from 1...N (0 is the start-and-finish value) AvmAssert(publicIndex > 0); // for "well-formed" (ie, generated with ASC or other reasonable compiler), // we should only end up calling this function once per iteration, with publicIndex == 1 .... // however, we don't assert here because it *is* legal to call it with other values // (in fact, our abcasm tests do so), but such usage is now dramatically slower than // it used to be. // AvmAssert(publicIndex == 1); const Atom* const atoms = getAtoms(); int const cap = getCapacity(); uintptr_t const dontEnumMask = (m_atomsAndFlags & kDontEnumBit); for (int i = 0; i < cap; i += 2) { Atom const a = atoms[i]; // don't need to check against EMPTY and DELETED since they won't be kIntptrType MMGC_STATIC_ASSERT(atomKind(EMPTY) != kIntptrType); MMGC_STATIC_ASSERT(atomKind(DELETED) != kIntptrType); // don't need to check for dontEnumMask because it will make kIntptrType appear as kDoubleType MMGC_STATIC_ASSERT((kIntptrType | kDontEnumBit) != kIntptrType); if (atomKind(a) == kIntptrType) { if (--publicIndex == 0) return i | kIterIndexIsIntptr; } } for (int i = 0; i < cap; i += 2) { Atom const a = atoms[i]; if (a != EMPTY && a != DELETED && atomKind(a) != kIntptrType && !(uintptr_t(a) & dontEnumMask)) { if (--publicIndex == 0) return i; } } // well-formed code can get here if there are no enumerable keys in the table. // return a value that is always > getCapacity; // since all callers check vs. cap this neuters the result return cap+1; } const Atom* InlineHashtable::expandForIterIndex() { const Atom* atoms = getAtoms(); int const cap = getCapacity(); if (!hasIterIndex()) { GC* gc = GC::GetGC(atoms); MMGC_MEM_TYPE(gc->FindBeginningGuarded(this)); AtomContainer* atomContainer = createAtoms(gc, cap+2); Atom* newAtoms = atomContainer->atoms; // no need to rehash: size is the same as far as atoms are concerned VMPI_memcpy(newAtoms, atoms, cap*sizeof(Atom)); freeAtoms(); m_atomsAndFlags |= kHasIterIndex; setAtoms(atomContainer); return newAtoms; } return atoms; } // call this method using the previous value returned // by this method starting with 0, until 0 is returned. int InlineHashtable::next(int index) { // we need our iter-index fields to iterate. // since most objects don't ever iterate this way, // they aren't always preallocated... if we get here and // they aren't, reallocate first. const Atom* atoms = expandForIterIndex(); AvmAssert(hasIterIndex()); // @todo int const cap = getCapacity(); intptr_t* iterIndices = (intptr_t*)(atoms + cap); // // iterIndices[0] == the expected public index we receive as arg // iterIndices[1] == the real internal index that corresponds to it, // with bit 31 set if the entry was an intptr. // int publicIndex = int(iterIndices[0]); int realIndex = int(iterIndices[1]); if (!index) { // if index == 0, we're starting the iteration; prime the pump by finding // the first enumerable int atom (or first enumerable non-int if we have no ints) // in this case we ignore whatever is currently in iterIndices publicIndex = 1; realIndex = publicIterIndexToRealIndex(1); if ((realIndex & ~kIterIndexIsIntptr) >= cap) { publicIndex = realIndex = 0; goto done; } } else { // we'll never return an index larger than cap, but malicious ABC could try to feed us one if (index > cap) { // the spec doesn't explicitly say this is an error, handle gracefully publicIndex = realIndex = 0; goto done; } if (publicIndex == 0 || publicIndex != index) { // if publicIndex == 0, it's an uninitialized value (eg post-grow()),and realIndex is garbage. // // if publicIndex is nonzero but doesn't match index, someone is passing // us an arbitrary value, rather than the value we // last returned; this won't happen with "normal" ABC but hand-assembled // ABC could do so, so be prepared to do something reasonable. publicIndex = index; realIndex = publicIterIndexToRealIndex(index); if ((realIndex & ~kIterIndexIsIntptr) >= cap) { publicIndex = realIndex = 0; goto done; } } else { // You'd think this assert would be appropriate, but you'd be wrong: // if code iterates over properties and marks them dontEnum, // this will not hold true // AvmAssert(realIndex == publicIterIndexToRealIndex(publicIndex)); } // realIndex should now always point to a valid key AvmAssert((realIndex & ~kIterIndexIsIntptr) < cap); AvmAssert(!(realIndex & 1)); // ok, advance to the next candidate... if we are currently on an int atom, // advance to the next int atom. if we run out, advance to non-ints. Note, // test kIterIndexIsIntptr in realIndex to determine the type, // since the value there could have been changed to DELETED since we // were last called... publicIndex += 1; // assume we'll find something uintptr_t dontEnumMask = (m_atomsAndFlags & kDontEnumBit); if (realIndex & kIterIndexIsIntptr) { realIndex &= ~kIterIndexIsIntptr; for (realIndex += 2; realIndex < cap; realIndex += 2) { Atom const a = atoms[realIndex]; // don't need to check against EMPTY and DELETED since they won't be kIntptrType. // don't need to check for dontEnumMask because it will make kIntptrType appear as kDoubleType if (atomKind(a) == kIntptrType) { realIndex |= kIterIndexIsIntptr; goto done; } } // reset for search thru object list (-2 so we start at zero) realIndex = -2; } // No enumerable kIntptrType entries left, try the non-kIntptrType for (realIndex += 2; realIndex < cap; realIndex += 2) { Atom const a = atoms[realIndex]; if (a != EMPTY && a != DELETED && atomKind(a) != kIntptrType && !(uintptr_t(a) & dontEnumMask)) { goto done; } } // oh well, nothing left publicIndex = realIndex = 0; } done: iterIndices[0] = publicIndex; iterIndices[1] = realIndex; return publicIndex; } Atom InlineHashtable::keyAt(int i) { // if we get here without this bit being set, someone called us without // calling next() first -- a no-no. We'll still work, but we will have // to search for the right index, which is slower. AvmAssert(hasIterIndex()); AvmAssert(i > 0); Atom* const atoms = getAtoms(); int const cap = getCapacity(); intptr_t* const iterIndices = (intptr_t*)(atoms + cap); int realIndex = (hasIterIndex() && iterIndices[0] == i) ? (int)iterIndices[1] : publicIterIndexToRealIndex(i); realIndex &= ~kIterIndexIsIntptr; return realIndex < cap ? removeDontEnumMask(atoms[realIndex]) : nullStringAtom; } Atom InlineHashtable::valueAt(int i) { // if we get here without this bit being set, someone called us without // calling next() first -- a no-no. We'll still work, but we will have // to search for the right index, which is slower. AvmAssert(hasIterIndex()); AvmAssert(i > 0); Atom* const atoms = getAtoms(); int const cap = getCapacity(); intptr_t* const iterIndices = (intptr_t*)(atoms + cap); int realIndex = (hasIterIndex() && iterIndices[0] == i) ? (int)iterIndices[1] : publicIterIndexToRealIndex(i); realIndex &= ~kIterIndexIsIntptr; realIndex += 1; return realIndex < cap ? atoms[realIndex] : undefinedAtom; } void InlineHashtable::removeKeyValuePairAtPublicIndex(int i) { AvmAssert(hasIterIndex()); AvmAssert(i > 0); Atom* const atoms = getAtoms(); int const cap = getCapacity(); intptr_t* const iterIndices = (intptr_t*)(atoms + cap); int realIndex = (hasIterIndex() && iterIndices[0] == i) ? (int)iterIndices[1] : publicIterIndexToRealIndex(i); realIndex &= ~kIterIndexIsIntptr; if (realIndex < cap) { // decrement refcount as necessary. AvmCore::atomWriteBarrier_dtor(&atoms[realIndex]); AvmCore::atomWriteBarrier_dtor(&atoms[realIndex+1]); // calls above set the slot to 0, we want DELETED atoms[realIndex] = DELETED; atoms[realIndex+1] = DELETED; setHasDeletedItems(); } } bool InlineHashtable::getAtomPropertyIsEnumerable(Atom name) const { if (hasDontEnumSupport()) { const Atom* atoms = getAtoms(); int i = find(name, atoms, getCapacity()); if (Atom(atoms[i]&~kDontEnumBit) == name) { return (atoms[i]&kDontEnumBit) == 0; } else { return false; } } else { return contains(name); } } void InlineHashtable::setAtomPropertyIsEnumerable(Atom name, bool enumerable) { if (hasDontEnumSupport()) { Atom* atoms = getAtoms(); int i = find(name, atoms, getCapacity()); if (Atom(atoms[i]&~kDontEnumBit) == name) { atoms[i] = (atoms[i]&~kDontEnumBit) | (enumerable ? 0 : kDontEnumBit); } } } /*virtual*/ HeapHashtableRC::~HeapHashtableRC() { ht.destroy(); } /*virtual*/ HeapHashtable::~HeapHashtable() { ht.destroy(); } /*virtual*/ int HeapHashtable::next(int index) { return ht.next(index); } /*virtual*/ void HeapHashtable::add(Atom name, Atom value, Toplevel* toplevel /*=NULL*/) { ht.add(name, value, toplevel); } /*virtual*/ Atom HeapHashtable::get(Atom name) { return ht.get(name); } /*virtual*/ Atom HeapHashtable::remove(Atom name) { return ht.remove(name); } /*virtual*/ bool HeapHashtable::contains(Atom name) const { return ht.contains(name); } /*virtual*/ bool HeapHashtable::weakKeys() const { return false; } /*virtual*/ bool HeapHashtable::weakValues() const { return false; } #ifdef DEBUGGER /*virtual*/ uint64_t HeapHashtable::bytesUsed() const { return GC::Size(this) + ht.bytesUsed(); } #endif /*virtual*/ Atom WeakKeyHashtable::get(Atom key) { return ht.get(getKey(key)); } /*virtual*/ Atom WeakKeyHashtable::remove(Atom key) { return ht.remove(getKey(key)); } /*virtual*/ bool WeakKeyHashtable::contains(Atom key) const { return ht.contains(getKey(key)); } /*virtual*/ bool WeakKeyHashtable::weakKeys() const { return true; } Atom WeakKeyHashtable::getKey(Atom key) const { // this gets a weak ref number, ie double keys, okay I guess if(AvmCore::isPointer(key)) { // Don't know if key is GCObject or GCFinalizedObject so can't go via the // GetWeakRef methods on those classes; go directly to GC GCWeakRef *weakRef = GC::GetWeakRef(atomPtr(key)); key = AvmCore::genericObjectToAtom(weakRef); } return key; } /*virtual*/ void WeakKeyHashtable::add(Atom key, Atom value, Toplevel* toplevel) { if (ht.put(getKey(key), value)) { if (ht.isFull()) { prune(); ht.grow(toplevel); } } } void WeakKeyHashtable::prune() { Atom* atoms = ht.getAtoms(); for(int i=0, n=ht.getCapacity(); iisNull()) { ht.deletePairAt(i); } } } } /*virtual*/ int WeakKeyHashtable::next(int index) { for (;;) { index = ht.next(index); if (index != 0) { Atom const a = ht.keyAt(index); if (AvmCore::isGenericObject(a)) { GCWeakRef* weakRef = (GCWeakRef*)AvmCore::atomToGenericObject(a); if (!weakRef->get()) { ht.removeKeyValuePairAtPublicIndex(index); continue; } } } return index; } } Atom WeakValueHashtable::getValue(Atom key, Atom value) { if(AvmCore::isGenericObject(value)) { GCWeakRef *wr = (GCWeakRef*)AvmCore::atomToGenericObject(value); if(!wr->isNull()) { // Note wr could be a pointer to a double, that's what this is for. // Use 'peek' to avoid marking the object unless it actually is an // RCObject. Atom* atoms = ht.getAtoms(); if(GC::GetGC(atoms)->IsRCObject(wr->peek())) { union { GCObject* o; AvmPlusScriptableObject* so; }; o = wr->get(); value = so->toAtom(); } else { AvmAssert(false); } } else { remove(key); value = undefinedAtom; } } return value; } /*virtual*/ void WeakValueHashtable::add(Atom key, Atom value, Toplevel* toplevel) { if(AvmCore::isPointer(value)) { // Don't know if value is GCObject or GCFinalizedObject so can't go via the // GetWeakRef methods on those classes; go directly to GC GCWeakRef *wf = GC::GetWeakRef(atomPtr(value)); value = AvmCore::genericObjectToAtom(wf); } if (ht.put(key, value)) { if (ht.isFull()) { prune(); ht.grow(toplevel); } } } /*virtual*/ Atom WeakValueHashtable::get(Atom key) { return getValue(key, ht.get(key)); } /*virtual*/ Atom WeakValueHashtable::remove(Atom key) { return getValue(key, ht.remove(key)); } /*virtual*/ bool WeakValueHashtable::weakValues() const { return true; } void WeakValueHashtable::prune() { Atom* atoms = ht.getAtoms(); for(int i=0, n=ht.getCapacity(); iisNull()) { ht.deletePairAt(i); } } } } }