lib/VM/SegmentedArray.cpp (360 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "hermes/VM/SegmentedArray.h" #include "hermes/VM/GCPointer-inline.h" #include "hermes/VM/HermesValue-inline.h" namespace hermes { namespace vm { const VTable SegmentedArray::Segment::vt( CellKind::SegmentKind, cellSize<SegmentedArray::Segment>(), nullptr, nullptr, nullptr, nullptr, VTable::HeapSnapshotMetadata{ HeapSnapshot::NodeType::Array, nullptr, nullptr, nullptr, nullptr}); void SegmentBuildMeta(const GCCell *cell, Metadata::Builder &mb) { const auto *self = static_cast<const SegmentedArray::Segment *>(cell); mb.setVTable(&SegmentedArray::Segment::vt); mb.addArray("data", self->data_, &self->length_, sizeof(GCHermesValue)); } PseudoHandle<SegmentedArray::Segment> SegmentedArray::Segment::create( Runtime &runtime) { // NOTE: This needs to live in the cpp file instead of the header because it // uses PseudoHandle, which requires a specialization of IsGCObject for the // type it constructs. return createPseudoHandle(runtime.makeAFixed<Segment>()); } void SegmentedArray::Segment::setLength(Runtime &runtime, uint32_t newLength) { const auto len = length(); if (newLength > len) { // Length is increasing, fill with emptys. GCHermesValue::uninitialized_fill( data_ + len, data_ + newLength, HermesValue::encodeEmptyValue(), &runtime.getHeap()); length_.store(newLength, std::memory_order_release); } else if (newLength < len) { // If length is decreasing a write barrier needs to be done. GCHermesValue::rangeUnreachableWriteBarrier( data_ + newLength, data_ + len, &runtime.getHeap()); length_.store(newLength, std::memory_order_release); } } const VTable SegmentedArray::vt( CellKind::SegmentedArrayKind, /*variableSize*/ 0, nullptr, nullptr, nullptr, _trimSizeCallback, VTable::HeapSnapshotMetadata{ HeapSnapshot::NodeType::Array, nullptr, nullptr, nullptr, nullptr}); void SegmentedArrayBuildMeta(const GCCell *cell, Metadata::Builder &mb) { const auto *self = static_cast<const SegmentedArray *>(cell); mb.setVTable(&SegmentedArray::vt); mb.addArray( "slots", self->inlineStorage(), &self->numSlotsUsed_, sizeof(GCHermesValue)); } CallResult<PseudoHandle<SegmentedArray>> SegmentedArray::create( Runtime &runtime, size_type capacity) { if (LLVM_UNLIKELY(capacity > maxElements())) { return throwExcessiveCapacityError(runtime, capacity); } // Leave the segments as null. Whenever the size is changed, the segments will // be allocated. // Note that this means the capacity argument won't be reflected in capacity() // if it is larger than the inline storage space. That is in order to avoid // having an extra field to track, and the upper bound of "size" can be used // instead. const auto allocSize = allocationSizeForCapacity(capacity); return createPseudoHandle(runtime.makeAVariable<SegmentedArray>(allocSize)); } CallResult<PseudoHandle<SegmentedArray>> SegmentedArray::createLongLived( Runtime &runtime, size_type capacity) { if (LLVM_UNLIKELY(capacity > maxElements())) { return throwExcessiveCapacityError(runtime, capacity); } // Leave the segments as null. Whenever the size is changed, the segments will // be allocated. const auto allocSize = allocationSizeForCapacity(capacity); return createPseudoHandle( runtime.makeAVariable<SegmentedArray, HasFinalizer::No, LongLived::Yes>( allocSize)); } CallResult<PseudoHandle<SegmentedArray>> SegmentedArray::create(Runtime &runtime, size_type capacity, size_type size) { auto arrRes = create(runtime, capacity); if (LLVM_UNLIKELY(arrRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } PseudoHandle<SegmentedArray> self = std::move(*arrRes); // TODO T25663446: This is potentially optimizable to iterate over the inline // storage and the segments separately. self = increaseSize(runtime, std::move(self), size); return self; } SegmentedArray::size_type SegmentedArray::capacity() const { const auto numSlotsUsed = numSlotsUsed_.load(std::memory_order_relaxed); if (numSlotsUsed <= kValueToSegmentThreshold) { // In the case where the size is less than the number of inline elements, // the capacity is at most slotCapacity, or the segment threshold if slot // capacity goes beyond that. return std::min(slotCapacity(), size_type{kValueToSegmentThreshold}); } else { // Any slot after numSlotsUsed_ is guaranteed to be null. return kValueToSegmentThreshold + (numSlotsUsed - kValueToSegmentThreshold) * Segment::kMaxLength; } } SegmentedArray::size_type SegmentedArray::totalCapacityOfSpine() const { const auto slotCap = slotCapacity(); if (slotCap <= kValueToSegmentThreshold) { return slotCap; } else { return kValueToSegmentThreshold + (slotCap - kValueToSegmentThreshold) * Segment::kMaxLength; } } ExecutionStatus SegmentedArray::push_back( MutableHandle<SegmentedArray> &self, Runtime &runtime, Handle<> value) { auto oldSize = self->size(); if (growRight(self, runtime, 1) == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } auto &elm = self->atRef(oldSize); new (&elm) GCHermesValue(*value, &runtime.getHeap()); return ExecutionStatus::RETURNED; } ExecutionStatus SegmentedArray::resize( MutableHandle<SegmentedArray> &self, Runtime &runtime, size_type newSize) { if (newSize > self->size()) { return growRight(self, runtime, newSize - self->size()); } else if (newSize < self->size()) { self->shrinkRight(runtime, self->size() - newSize); } return ExecutionStatus::RETURNED; } ExecutionStatus SegmentedArray::resizeLeft( MutableHandle<SegmentedArray> &self, Runtime &runtime, size_type newSize) { if (newSize == self->size()) { return ExecutionStatus::RETURNED; } else if (newSize > self->size()) { return growLeft(self, runtime, newSize - self->size()); } else { self->shrinkLeft(runtime, self->size() - newSize); return ExecutionStatus::RETURNED; } } void SegmentedArray::resizeWithinCapacity( SegmentedArray *self, Runtime &runtime, size_type newSize) { const size_type currSize = self->size(); assert( newSize <= self->capacity() && "Cannot resizeWithinCapacity to a size not within capacity"); if (newSize > currSize) { self->increaseSizeWithinCapacity(runtime, newSize - currSize); } else if (newSize < currSize) { self->shrinkRight(runtime, currSize - newSize); } } ExecutionStatus SegmentedArray::throwExcessiveCapacityError( Runtime &runtime, size_type capacity) { assert( capacity > maxElements() && "Shouldn't call this without first checking that capacity is big"); return runtime.raiseRangeError( TwineChar16( "Requested an array size larger than the max allowable: Requested elements = ") + capacity + ", max elements = " + maxElements()); } void SegmentedArray::allocateSegment( Runtime &runtime, Handle<SegmentedArray> self, SegmentNumber segment) { assert( self->segmentAtPossiblyUnallocated(segment)->isEmpty() && "Allocating into a non-empty segment"); PseudoHandle<Segment> c = Segment::create(runtime); self->segmentAtPossiblyUnallocated(segment)->set( c.getHermesValue(), &runtime.getHeap()); } ExecutionStatus SegmentedArray::growRight( MutableHandle<SegmentedArray> &self, Runtime &runtime, size_type amount) { if (self->size() + amount <= self->totalCapacityOfSpine()) { increaseSize(runtime, self, amount); return ExecutionStatus::RETURNED; } const auto newSize = self->size() + amount; // Allocate a new SegmentedArray according to the resize policy. auto arrRes = create(runtime, calculateNewCapacity(self->size(), newSize)); if (arrRes == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } PseudoHandle<SegmentedArray> newSegmentedArray = std::move(*arrRes); // Copy inline storage and segments over. // Do this with raw pointers so that the range write barrier occurs. const auto numSlotsUsed = self->numSlotsUsed_.load(std::memory_order_relaxed); GCHermesValue::uninitialized_copy( self->inlineStorage(), self->inlineStorage() + numSlotsUsed, newSegmentedArray->inlineStorage(), &runtime.getHeap()); // Set the size of the new array to be the same as the old array's size. newSegmentedArray->numSlotsUsed_.store( numSlotsUsed, std::memory_order_release); newSegmentedArray = increaseSize(runtime, std::move(newSegmentedArray), amount); // Assign back to self. self = newSegmentedArray.get(); return ExecutionStatus::RETURNED; } ExecutionStatus SegmentedArray::growLeft( MutableHandle<SegmentedArray> &self, Runtime &runtime, size_type amount) { if (self->size() + amount <= self->totalCapacityOfSpine()) { growLeftWithinCapacity(runtime, self, amount); return ExecutionStatus::RETURNED; } const auto newSize = self->size() + amount; auto arrRes = create(runtime, calculateNewCapacity(self->size(), newSize), newSize); if (arrRes == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } PseudoHandle<SegmentedArray> newSegmentedArray = std::move(*arrRes); // Copy element-by-element, since a shift would need to happen anyway. // Since self and newSegmentedArray are distinct, don't need to worry about // order. GCHermesValue::copy( self->begin(), self->end(), newSegmentedArray->begin() + amount, &runtime.getHeap()); // Assign back to self. self = newSegmentedArray.get(); return ExecutionStatus::RETURNED; } void SegmentedArray::growLeftWithinCapacity( Runtime &runtime, PseudoHandle<SegmentedArray> self, size_type amount) { assert( self->size() + amount <= self->totalCapacityOfSpine() && "Cannot grow higher than capacity"); // Fill with empty values at the end to simplify the write barrier. self = increaseSize(runtime, std::move(self), amount); // Copy the range from the beginning to the end. GCHermesValue::copy_backward( self->begin(), self->end() - amount, self->end(), &runtime.getHeap()); // Fill the beginning with empty values. GCHermesValue::fill( self->begin(), self->begin() + amount, HermesValue::encodeEmptyValue(), &runtime.getHeap()); } void SegmentedArray::shrinkRight(Runtime &runtime, size_type amount) { decreaseSize(runtime, amount); } void SegmentedArray::shrinkLeft(Runtime &runtime, size_type amount) { // Copy the end values leftwards to the beginning. GCHermesValue::copy(begin() + amount, end(), begin(), &runtime.getHeap()); // Now that all the values are moved down, fill the end with empty values. decreaseSize(runtime, amount); } void SegmentedArray::increaseSizeWithinCapacity( Runtime &runtime, size_type amount) { // This function has the same logic as increaseSize, but removes some // complexity from avoiding dealing with alllocations. const auto empty = HermesValue::encodeEmptyValue(); const auto currSize = size(); const auto finalSize = currSize + amount; assert( finalSize <= capacity() && "Cannot use increaseSizeWithinCapacity without checking for capacity first"); if (finalSize <= kValueToSegmentThreshold) { // currSize and finalSize are inside inline storage, bump and fill. GCHermesValue::uninitialized_fill( inlineStorage() + currSize, inlineStorage() + finalSize, empty, &runtime.getHeap()); // Set the final size. numSlotsUsed_.store(finalSize, std::memory_order_release); return; } // Since this change is within capacity, it is at most filling up a single // segment. const SegmentNumber segment = toSegment(finalSize - 1); const auto segmentLength = toInterior(finalSize - 1) + 1; // Fill the inline slots if necessary, and the single segment. if (currSize < kValueToSegmentThreshold) { GCHermesValue::uninitialized_fill( inlineStorage() + currSize, inlineStorage() + kValueToSegmentThreshold, empty, &runtime.getHeap()); } segmentAt(segment)->setLength(runtime, segmentLength); } PseudoHandle<SegmentedArray> SegmentedArray::increaseSize( Runtime &runtime, PseudoHandle<SegmentedArray> self, size_type amount) { const auto empty = HermesValue::encodeEmptyValue(); const auto currSize = self->size(); const auto finalSize = currSize + amount; if (finalSize <= self->capacity()) { self->increaseSizeWithinCapacity(runtime, amount); return self; } // Inline slots must be reserved by the caller. Since finalSize is greater // than the capacity, we know that it must require adding segments. assert(finalSize > kValueToSegmentThreshold); // currSize might be in inline storage, but finalSize is definitely in // segments. // Allocate missing segments after filling inline storage. if (currSize <= kValueToSegmentThreshold) { // Segments will need to be allocated, if the old size didn't have the // inline storage filled up, fill it up now. GCHermesValue::uninitialized_fill( self->inlineStorage() + currSize, self->inlineStorage() + kValueToSegmentThreshold, empty, &runtime.getHeap()); // Set the size to the inline storage threshold. self->numSlotsUsed_.store( kValueToSegmentThreshold, std::memory_order_release); } // NOTE: during this function, allocations can happen. // If one of these allocations triggers a full compacting GC, then the array // currently being increased might have its capacity shrunk to match its // numSlotsUsed. So, increase numSlotsUsed immediately to its final value // before the allocations happen so it isn't shrunk, and also fill with empty // values so that any mark passes don't fail. // The segments should all have length 0 until allocations are finished, so // that uninitialized memory is not scanned inside the segments. Once // allocations are finished, go back and fixup the lengths. const SegmentNumber startSegment = currSize <= kValueToSegmentThreshold ? 0 : toSegment(currSize - 1); const SegmentNumber lastSegment = toSegment(finalSize - 1); const auto newNumSlotsUsed = numSlotsForCapacity(finalSize); // Put empty values into all of the added slots so that the memory is not // uninitialized during marking. GCHermesValue::uninitialized_fill( self->inlineStorage() + self->numSlotsUsed_.load(std::memory_order_relaxed), self->inlineStorage() + newNumSlotsUsed, empty, &runtime.getHeap()); self->numSlotsUsed_.store(newNumSlotsUsed, std::memory_order_release); // Allocate a handle to track the current array. auto selfHandle = runtime.makeHandle(std::move(self)); // Allocate each segment. if (startSegment <= lastSegment && selfHandle->segmentAtPossiblyUnallocated(startSegment)->isEmpty()) { // The start segment might already be allocated if it was half full when we // increase the size. allocateSegment(runtime, selfHandle, startSegment); } for (auto i = startSegment + 1; i <= lastSegment; ++i) { // All segments except the start need to become allocated. allocateSegment(runtime, selfHandle, i); } // Now that all allocations have occurred, set the lengths inside each // segment, and optionally fill. for (auto i = startSegment; i <= lastSegment; ++i) { // If its the last chunk, set to the length required by any leftover // elements. const auto segmentLength = i == lastSegment ? toInterior(finalSize - 1) + 1 : Segment::kMaxLength; selfHandle->segmentAt(i)->setLength(runtime, segmentLength); } self = selfHandle; return self; } void SegmentedArray::decreaseSize(Runtime &runtime, size_type amount) { const auto initialSize = size(); const auto initialNumSlots = numSlotsUsed_.load(std::memory_order_relaxed); assert(amount <= initialSize && "Cannot decrease size past zero"); const auto finalSize = initialSize - amount; const auto finalNumSlots = numSlotsForCapacity(finalSize); assert( finalNumSlots <= initialNumSlots && "Should not be increasing the number of slots"); if (finalSize > kValueToSegmentThreshold) { // Set the new last used segment's length to be the leftover. segmentAt(toSegment(finalSize - 1)) ->setLength(runtime, toInterior(finalSize - 1) + 1); } // Before shrinking, do a snapshot write barrier for the elements being // removed. GCHermesValue::rangeUnreachableWriteBarrier( inlineStorage() + finalNumSlots, inlineStorage() + initialNumSlots, &runtime.getHeap()); numSlotsUsed_.store(finalNumSlots, std::memory_order_release); } gcheapsize_t SegmentedArray::_trimSizeCallback(const GCCell *cell) { const auto *self = reinterpret_cast<const SegmentedArray *>(cell); // This array will shrink so that it has the same slot capacity as the slot // size. return allocationSizeForSlots( self->numSlotsUsed_.load(std::memory_order_relaxed)); } } // namespace vm } // namespace hermes