Commit 76d74489 authored by Samuel Groß's avatar Samuel Groß Committed by V8 LUCI CQ

[sandbox] Implement external pointer table compaction

The external pointer table is already to some degree self-compacting: as
the freelist is sorted in ascending order, free entries at the start of
the table should quickly fill up. However, any live entry at the end of
the table makes it impossible to shrink the table, thereby causing
higher memory consumption. To solve this, this CL implements a simple
table compaction algorithm, used when the freelist has become
sufficiently large (currently >= 10% of the table capacity):
 - The goal of the algorithm is to shrink the table by freelist_size/2
   entries at the end of compaction (during sweeping).
 - At the start of the marking phase, the compaction area is computed as
   roughly [capacity - freelist_size/2, capacity).
 - When an entry is marked as alive that lies inside the compaction
   area, a new "relocation entry" is allocated for it from the freelist
   and the address of the handle for that entry is stored in that entry.
   If there are no more free entries before the compaction area,
   compaction is aborted. This is expected to happen rarely and is
   logged into a histogram.
 - During sweeping, all relocation entries are "resolved": the content
   of the old entry is copied into the new entry and the handle is
   updated to point to the new entry.
 - Finally, the table is shrunk and the last initial_freelist_size/2
   entries are decommitted.

See also the comments in the ExternalPointerTable class for more details.

Bug: v8:10391
Change-Id: I28d475c3596590e860421f0a054e2ad4dbebd487
Cq-Include-Trybots: luci.v8.try:v8_linux64_heap_sandbox_dbg_ng,v8_linux_arm64_sim_heap_sandbox_dbg_ng
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3794645Reviewed-by: 's avatarIgor Sheludko <ishell@chromium.org>
Reviewed-by: 's avatarMichael Lippautz <mlippautz@chromium.org>
Commit-Queue: Samuel Groß <saelo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#82484}
parent e417b339
......@@ -431,6 +431,8 @@ enum ExternalPointerTag : uint64_t {
// different type fails. It also doesn't have the mark bit set as free
// entries are (by definition) not alive.
kExternalPointerFreeEntryTag = MAKE_TAG(0, 0b11111111),
// Evacuation entries are used during external pointer table compaction.
kEvacuationEntryTag = MAKE_TAG(1, 0b11100111),
ALL_EXTERNAL_POINTER_TAGS(EXTERNAL_POINTER_TAG_ENUM)
};
......@@ -531,10 +533,7 @@ class Internals {
kExternalPointerTableBufferOffset + kApiSystemPointerSize;
static const int kExternalPointerTableFreelistHeadOffset =
kExternalPointerTableCapacityOffset + kApiInt32Size;
static const int kExternalPointerTableMutexOffset =
kExternalPointerTableFreelistHeadOffset + kApiInt32Size;
static const int kExternalPointerTableSize =
kExternalPointerTableMutexOffset + kApiSystemPointerSize;
static const int kExternalPointerTableSize = 4 * kApiSystemPointerSize;
// IsolateData layout guarantees.
static const int kIsolateCageBaseOffset = 0;
......
......@@ -3362,8 +3362,6 @@ void Isolate::CheckIsolateLayout() {
Internals::kExternalPointerTableCapacityOffset);
CHECK_EQ(static_cast<int>(OFFSET_OF(ExternalPointerTable, freelist_head_)),
Internals::kExternalPointerTableFreelistHeadOffset);
CHECK_EQ(static_cast<int>(OFFSET_OF(ExternalPointerTable, mutex_)),
Internals::kExternalPointerTableMutexOffset);
CHECK_EQ(static_cast<int>(sizeof(ExternalPointerTable)),
Internals::kExternalPointerTableSize);
CHECK_EQ(static_cast<int>(sizeof(ExternalPointerTable)),
......@@ -5791,11 +5789,12 @@ ExternalPointerHandle
Isolate::InsertWaiterQueueNodeIntoSharedExternalPointerTable(Address node) {
DCHECK_NE(kNullAddress, node);
ExternalPointerHandle handle;
if (waiter_queue_node_external_pointer_.IsJust()) {
handle = waiter_queue_node_external_pointer_.FromJust();
if (waiter_queue_node_external_pointer_handle_ !=
kNullExternalPointerHandle) {
handle = waiter_queue_node_external_pointer_handle_;
} else {
handle = shared_external_pointer_table().Allocate();
waiter_queue_node_external_pointer_ = Just(handle);
handle = shared_external_pointer_table().AllocateEntry();
waiter_queue_node_external_pointer_handle_ = handle;
}
DCHECK_NE(0, handle);
shared_external_pointer_table().Set(handle, node, kWaiterQueueNodeTag);
......
......@@ -40,6 +40,7 @@
#include "src/objects/debug-objects.h"
#include "src/objects/js-objects.h"
#include "src/runtime/runtime.h"
#include "src/sandbox/external-pointer.h"
#include "src/utils/allocation.h"
#ifdef DEBUG
......@@ -1971,8 +1972,8 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory {
&isolate_data_.shared_external_pointer_table_);
}
Maybe<ExternalPointerHandle> GetWaiterQueueNodeExternalPointer() const {
return waiter_queue_node_external_pointer_;
ExternalPointerHandle* GetWaiterQueueNodeExternalPointerHandleLocation() {
return &waiter_queue_node_external_pointer_handle_;
}
ExternalPointerHandle InsertWaiterQueueNodeIntoSharedExternalPointerTable(
......@@ -2449,8 +2450,8 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory {
#ifdef V8_COMPRESS_POINTERS
// The external pointer handle to the Isolate's main thread's WaiterQueueNode.
// It is used to wait for JS-exposed mutex or condition variable.
Maybe<ExternalPointerHandle> waiter_queue_node_external_pointer_ =
Nothing<ExternalPointerHandle>();
ExternalPointerHandle waiter_queue_node_external_pointer_handle_ =
kNullExternalPointerHandle;
#endif
#if DEBUG
......
......@@ -262,6 +262,10 @@ void IncrementalMarking::StartMarking() {
is_compacting_ = collector_->StartCompaction(
MarkCompactCollector::StartCompactionMode::kIncremental);
#ifdef V8_COMPRESS_POINTERS
heap_->isolate()->external_pointer_table().StartCompactingIfNeeded();
#endif // V8_COMPRESS_POINTERS
auto embedder_flags = heap_->flags_for_embedder_tracer();
{
TRACE_GC(heap()->tracer(),
......
......@@ -1021,6 +1021,9 @@ void MarkCompactCollector::Prepare() {
// be set up.
heap_->local_embedder_heap_tracer()->TracePrologue(embedder_flags);
}
#ifdef V8_COMPRESS_POINTERS
heap_->isolate()->external_pointer_table().StartCompactingIfNeeded();
#endif // V8_COMPRESS_POINTERS
}
heap_->FreeLinearAllocationAreas();
......@@ -2272,10 +2275,13 @@ void MarkCompactCollector::MarkObjectsFromClientHeaps() {
// Custom marking for the external pointer table entry used to hold
// client Isolates' WaiterQueueNode, which is used by JS mutexes and
// condition variables.
ExternalPointerHandle waiter_queue_ext;
if (client->GetWaiterQueueNodeExternalPointer().To(&waiter_queue_ext)) {
uint32_t index = waiter_queue_ext >> kExternalPointerIndexShift;
client->shared_external_pointer_table().Mark(index);
ExternalPointerHandle* handle_location =
client->GetWaiterQueueNodeExternalPointerHandleLocation();
ExternalPointerTable& table = client->shared_external_pointer_table();
ExternalPointerHandle handle =
base::AsAtomic32::Relaxed_Load(handle_location);
if (handle) {
table.Mark(handle, reinterpret_cast<Address>(handle_location));
}
#endif // V8_COMPRESS_POINTERS
});
......@@ -2926,9 +2932,12 @@ void MarkCompactCollector::ClearNonLiveReferences() {
{
TRACE_GC(heap()->tracer(),
GCTracer::Scope::MC_SWEEP_EXTERNAL_POINTER_TABLE);
isolate()->external_pointer_table().Sweep(isolate());
// External pointer table sweeping needs to happen before evacuating live
// objects as it may perform table compaction, which requires objects to
// still be at the same location as during marking.
isolate()->external_pointer_table().SweepAndCompact(isolate());
if (isolate()->owns_shareable_data()) {
isolate()->shared_external_pointer_table().Sweep(isolate());
isolate()->shared_external_pointer_table().SweepAndCompact(isolate());
}
}
#endif // V8_ENABLE_SANDBOX
......
......@@ -147,11 +147,10 @@ void MarkingVisitorBase<ConcreteVisitor, MarkingState>::VisitExternalPointer(
#ifdef V8_ENABLE_SANDBOX
if (IsSandboxedExternalPointerType(tag)) {
ExternalPointerHandle handle = slot.Relaxed_LoadHandle();
if (IsSharedExternalPointerType(tag)) {
shared_external_pointer_table_->Mark(handle);
} else {
external_pointer_table_->Mark(handle);
}
ExternalPointerTable* table = IsSharedExternalPointerType(tag)
? shared_external_pointer_table_
: external_pointer_table_;
table->Mark(handle, slot.address());
}
#endif // V8_ENABLE_SANDBOX
}
......
......@@ -116,7 +116,12 @@ namespace internal {
HR(wasm_num_lazy_compilations_60sec, V8.WasmNumLazyCompilations60Sec, 0, \
200000, 50) \
HR(wasm_num_lazy_compilations_120sec, V8.WasmNumLazyCompilations120Sec, 0, \
200000, 50)
200000, 50) \
/* Outcome of external pointer table compaction: kSuccess, */ \
/* kPartialSuccessor kAbortedDuringSweeping. See */ \
/* ExternalPointerTable::TableCompactionOutcome enum for more details */ \
HR(external_pointer_table_compaction_outcome, \
V8.ExternalPointerTableCompactionOutcome, 0, 2, 3)
#define NESTED_TIMED_HISTOGRAM_LIST(HT) \
/* Nested timer histograms allow distributions of nested timed results. */ \
......
......@@ -159,7 +159,7 @@ void ExternalPointerSlot::init(Isolate* isolate, Address value,
#ifdef V8_ENABLE_SANDBOX
if (IsSandboxedExternalPointerType(tag)) {
ExternalPointerTable& table = GetExternalPointerTableForTag(isolate, tag);
ExternalPointerHandle handle = table.Allocate();
ExternalPointerHandle handle = table.AllocateEntry();
table.Set(handle, value, tag);
// Use a Release_Store to ensure that the store of the pointer into the
// table is not reordered after the store of the handle. Otherwise, other
......
......@@ -36,7 +36,7 @@ V8_INLINE void InitExternalPointerField(Address field_address, Isolate* isolate,
#ifdef V8_ENABLE_SANDBOX
if (IsSandboxedExternalPointerType(tag)) {
ExternalPointerTable& table = GetExternalPointerTable<tag>(isolate);
ExternalPointerHandle handle = table.Allocate();
ExternalPointerHandle handle = table.AllocateEntry();
table.Set(handle, value, tag);
// Use a Release_Store to ensure that the store of the pointer into the
// table is not reordered after the store of the handle. Otherwise, other
......
......@@ -86,9 +86,7 @@ void ExternalPointerTable::TearDown() {
Address ExternalPointerTable::Get(ExternalPointerHandle handle,
ExternalPointerTag tag) const {
uint32_t index = handle >> kExternalPointerIndexShift;
DCHECK_LT(index, capacity());
uint32_t index = handle_to_index(handle);
Address entry = load_atomic(index);
DCHECK(!is_free(entry));
......@@ -101,9 +99,7 @@ void ExternalPointerTable::Set(ExternalPointerHandle handle, Address value,
DCHECK_EQ(0, value & kExternalPointerTagMask);
DCHECK(is_marked(tag));
uint32_t index = handle >> kExternalPointerIndexShift;
DCHECK_LT(index, capacity());
uint32_t index = handle_to_index(handle);
store_atomic(index, value | tag);
}
......@@ -113,15 +109,14 @@ Address ExternalPointerTable::Exchange(ExternalPointerHandle handle,
DCHECK_EQ(0, value & kExternalPointerTagMask);
DCHECK(is_marked(tag));
uint32_t index = handle >> kExternalPointerIndexShift;
DCHECK_LT(index, capacity());
uint32_t index = handle_to_index(handle);
Address entry = exchange_atomic(index, value | tag);
DCHECK(!is_free(entry));
return entry & ~tag;
}
ExternalPointerHandle ExternalPointerTable::Allocate() {
ExternalPointerHandle ExternalPointerTable::AllocateInternal(
bool is_evacuation_entry) {
DCHECK(is_initialized());
uint32_t index;
......@@ -134,6 +129,10 @@ ExternalPointerHandle ExternalPointerTable::Allocate() {
// thread to read a freelist entry before it has been properly initialized.
uint32_t freelist_head = base::Acquire_Load(&freelist_head_);
if (!freelist_head) {
// Evacuation entries must be allocated below the start of the evacuation
// area so there's no point in growing the table.
if (is_evacuation_entry) return kNullExternalPointerHandle;
// Freelist is empty. Need to take the lock, then attempt to grow the
// table if no other thread has done it in the meantime.
base::MutexGuard guard(mutex_);
......@@ -152,26 +151,78 @@ ExternalPointerHandle ExternalPointerTable::Allocate() {
DCHECK_LT(freelist_head, capacity());
index = freelist_head;
// The next free element is stored in the lower 32 bits of the entry.
uint32_t new_freelist_head = static_cast<uint32_t>(load_atomic(index));
if (is_evacuation_entry && index >= start_of_evacuation_area_)
return kNullExternalPointerHandle;
Address entry = load_atomic(index);
uint32_t new_freelist_head = extract_next_entry_from_freelist_entry(entry);
uint32_t old_val = base::Relaxed_CompareAndSwap(
&freelist_head_, freelist_head, new_freelist_head);
success = old_val == freelist_head;
}
return index << kExternalPointerIndexShift;
return index_to_handle(index);
}
void ExternalPointerTable::Mark(ExternalPointerHandle handle) {
static_assert(sizeof(base::Atomic64) == sizeof(Address));
ExternalPointerHandle ExternalPointerTable::AllocateEntry() {
constexpr bool is_evacuation_entry = false;
return AllocateInternal(is_evacuation_entry);
}
uint32_t index = handle >> kExternalPointerIndexShift;
DCHECK_LT(index, capacity());
ExternalPointerHandle ExternalPointerTable::AllocateEvacuationEntry() {
constexpr bool is_evacuation_entry = true;
return AllocateInternal(is_evacuation_entry);
}
uint32_t ExternalPointerTable::FreelistSize() {
Address entry = 0;
while (!is_free(entry)) {
uint32_t freelist_head = base::Relaxed_Load(&freelist_head_);
if (!freelist_head) {
return 0;
}
entry = load_atomic(freelist_head);
}
uint32_t freelist_size = extract_freelist_size_from_freelist_entry(entry);
DCHECK_LE(freelist_size, capacity());
return freelist_size;
}
void ExternalPointerTable::Mark(ExternalPointerHandle handle,
Address handle_location) {
static_assert(sizeof(base::Atomic64) == sizeof(Address));
DCHECK_EQ(handle, *reinterpret_cast<ExternalPointerHandle*>(handle_location));
uint32_t index = handle_to_index(handle);
// Check if the entry should be evacuated.
if (IsCompacting() && index >= start_of_evacuation_area_) {
ExternalPointerHandle new_handle = AllocateEvacuationEntry();
if (new_handle) {
DCHECK_LT(handle_to_index(new_handle), start_of_evacuation_area_);
uint32_t index = handle_to_index(new_handle);
// No need for an atomic store as the entry will only be accessed during
// sweeping.
store(index, make_evacuation_entry(handle_location));
} else {
// In this case, the application has allocated a sufficiently large
// number of entries from the freelist so that new entries would now be
// allocated inside the area that is being compacted. While it would be
// possible to shrink that area and continue compacting, we probably do
// not want to put more pressure on the freelist and so instead simply
// abort compaction here. Entries that have already been visited will
// still be compacted during Sweep, but there is no guarantee that any
// blocks at the end of the table will now be completely free.
start_of_evacuation_area_ = kTableCompactionAbortedMarker;
}
}
// Even if the entry is marked for evacuation, it still needs to be marked as
// alive as it may be visited during sweeping before being evacuation.
base::Atomic64 old_val = load_atomic(index);
DCHECK(!is_free(old_val));
base::Atomic64 new_val = set_mark_bit(old_val);
DCHECK(!is_free(old_val));
// We don't need to perform the CAS in a loop: if the new value is not equal
// to the old value, then the mutator must've just written a new value into
......
......@@ -17,10 +17,11 @@ namespace internal {
static_assert(sizeof(ExternalPointerTable) == ExternalPointerTable::kSize);
uint32_t ExternalPointerTable::Sweep(Isolate* isolate) {
uint32_t ExternalPointerTable::SweepAndCompact(Isolate* isolate) {
// There must not be any entry allocations while the table is being swept as
// that would not be safe. Set the freelist head to this special marker value
// to better catch any violation of this requirement.
uint32_t old_freelist_head = base::Relaxed_Load(&freelist_head_);
base::Release_Store(&freelist_head_, kTableIsCurrentlySweepingMarker);
// Keep track of the last block (identified by the index of its first entry)
......@@ -29,37 +30,111 @@ uint32_t ExternalPointerTable::Sweep(Isolate* isolate) {
const uint32_t last_block = capacity() - kEntriesPerBlock;
uint32_t last_in_use_block = last_block;
// When compacting, we can compute the number of unused blocks at the end of
// the table and skip those during sweeping.
if (IsCompacting()) {
DCHECK(IsAligned(start_of_evacuation_area_, kEntriesPerBlock));
TableCompactionOutcome outcome;
if (start_of_evacuation_area_ == kTableCompactionAbortedMarker) {
// Compaction was aborted during marking because the freelist grew to
// short. This is not great because there is now no guarantee that any
// blocks will be completely emtpy and so the entire table needs to be
// swept.
outcome = TableCompactionOutcome::kAbortedDuringMarking;
} else if (!old_freelist_head ||
old_freelist_head > start_of_evacuation_area_) {
// In this case, marking finished successfully, but the application
// afterwards allocated entries inside the area that is being compacted.
// In this case, we can still compute how many blocks at the end of the
// table are now empty.
if (old_freelist_head) {
last_in_use_block = RoundDown(old_freelist_head, kEntriesPerBlock);
}
outcome = TableCompactionOutcome::kPartialSuccess;
} else {
// Marking was successful so the entire area that we are compacting is now
// free.
last_in_use_block = start_of_evacuation_area_ - kEntriesPerBlock;
outcome = TableCompactionOutcome::kSuccess;
}
isolate->counters()->external_pointer_table_compaction_outcome()->AddSample(
static_cast<int>(outcome));
}
// Sweep top to bottom and rebuild the freelist from newly dead and
// previously freed entries while also clearing the marking bit on live
// entries. This way, the freelist ends up sorted by index, which helps
// defragment the table. This method must run either on the mutator thread or
// while the mutator is stopped.
uint32_t freelist_size = 0;
// entries and resolving evacuation entries table when compacting the table.
// This way, the freelist ends up sorted by index which already makes the
// table somewhat self-compacting and is required for the compaction
// algorithm so that evacuated entries are evacuated to the start of the
// table. This method must run either on the mutator thread or while the
// mutator is stopped.
uint32_t current_freelist_size = 0;
uint32_t current_freelist_head = 0;
// Skip the special null entry. This also guarantees that the first block
// will never be decommitted.
DCHECK_GE(capacity(), 1);
for (uint32_t i = capacity() - 1; i > 0; i--) {
uint32_t table_end = last_in_use_block + kEntriesPerBlock;
DCHECK(IsAligned(table_end, kEntriesPerBlock));
for (uint32_t i = table_end - 1; i > 0; i--) {
// No other threads are active during sweep, so there is no need to use
// atomic operations here.
Address entry = load(i);
if (!is_marked(entry)) {
store(i, make_freelist_entry(current_freelist_head));
if (is_evacuation_entry(entry)) {
// Resolve the evacuation entry: take the pointer to the handle from the
// evacuation entry, copy the entry to its new location, and finally
// update the handle to point to the new entry.
Address evacuation_entry = load(i);
ExternalPointerHandle* handle_location =
reinterpret_cast<ExternalPointerHandle*>(
extract_handle_location_from_evacuation_entry(evacuation_entry));
ExternalPointerHandle old_handle = *handle_location;
ExternalPointerHandle new_handle = index_to_handle(i);
DCHECK_GE(handle_to_index(old_handle), start_of_evacuation_area_);
DCHECK_LT(handle_to_index(new_handle), start_of_evacuation_area_);
Address entry_to_evacuate = load(handle_to_index(old_handle));
store(i, clear_mark_bit(entry_to_evacuate));
*handle_location = new_handle;
#ifdef DEBUG
// In debug builds, clobber the old entry so that any sharing of table
// entries is easily detected. Shared entries would require write
// barriers, so we'd like to avoid them. See the compaction algorithm
// explanation in external-pointer-table.h for more details.
constexpr Address kClobberedEntryMarker = static_cast<Address>(-1);
DCHECK_NE(entry_to_evacuate, kClobberedEntryMarker);
store(handle_to_index(old_handle), kClobberedEntryMarker);
#endif // DEBUG
// While we know that the old entry is now free, we don't add it to (the
// start of) the freelist because that would immediately cause new
// fragmentation when the next entry is allocated. Instead, we assume
// that the blocks out of which entries are evacuated will all be
// decommitted anyway after this loop, which is usually the case unless
// compaction was already aborted during marking.
} else if (!is_marked(entry)) {
current_freelist_size++;
Address entry =
make_freelist_entry(current_freelist_head, current_freelist_size);
store(i, entry);
current_freelist_head = i;
freelist_size++;
} else {
store(i, clear_mark_bit(entry));
}
if (last_in_use_block == i) {
// Finished iterating over the last in-use block. Now see if it is empty.
if (freelist_size == kEntriesPerBlock) {
// Finished iterating over the last in-use block. Now see if it is
// empty.
if (current_freelist_size == kEntriesPerBlock) {
// Block is completely empty, so mark it for decommitting.
last_in_use_block -= kEntriesPerBlock;
// Freelist is now empty again.
current_freelist_head = 0;
freelist_size = 0;
current_freelist_size = 0;
}
}
}
......@@ -73,16 +148,53 @@ uint32_t ExternalPointerTable::Sweep(Isolate* isolate) {
set_capacity(new_capacity);
VirtualAddressSpace* root_space = GetPlatformVirtualAddressSpace();
// The pages may contain stale pointers which could be abused by an
// attacker if they are still accessible, so use Decommit here which
// guarantees that the pages become inaccessible and will be zeroed out.
CHECK(root_space->DecommitPages(new_table_end, bytes_to_decommit));
}
if (IsCompacting()) {
StopCompacting();
}
base::Release_Store(&freelist_head_, current_freelist_head);
uint32_t num_active_entries = capacity() - freelist_size;
uint32_t num_active_entries = capacity() - current_freelist_size;
isolate->counters()->external_pointers_count()->AddSample(num_active_entries);
return num_active_entries;
}
void ExternalPointerTable::StartCompactingIfNeeded() {
// This method may be executed while other threads allocate entries from the
// freelist or even grow the table, thereby increasing the capacity. In that
// case, this method may use incorrect data to determine if table compaction
// is necessary. That's fine however since in the worst case, compaction will
// simply be aborted right away if the freelist became too small.
uint32_t freelist_size = FreelistSize();
uint32_t current_capacity = capacity();
// Current (somewhat arbitrary) heuristic: need compacting if the table is
// more than 1MB in size and is at least 10% empty.
uint32_t table_size = current_capacity * kSystemPointerSize;
double free_ratio = static_cast<double>(freelist_size) /
static_cast<double>(current_capacity);
bool should_compact = (table_size >= 1 * MB) && (free_ratio >= 0.10);
if (should_compact) {
uint32_t num_entries_to_free = freelist_size / 2;
num_entries_to_free = RoundDown(num_entries_to_free, kBlockSize);
DCHECK_GT(num_entries_to_free, 0);
// A non-zero value for this member indicates that compaction is running.
start_of_evacuation_area_ = current_capacity - num_entries_to_free;
}
}
void ExternalPointerTable::StopCompacting() {
DCHECK(IsCompacting());
start_of_evacuation_area_ = 0;
}
uint32_t ExternalPointerTable::Grow() {
// Freelist should be empty.
DCHECK_EQ(0, freelist_head_);
......@@ -105,10 +217,12 @@ uint32_t ExternalPointerTable::Grow() {
// Build freelist bottom to top, which might be more cache friendly.
uint32_t start = std::max<uint32_t>(old_capacity, 1); // Skip entry zero
uint32_t last = new_capacity - 1;
uint32_t current_freelist_size = 1;
for (uint32_t i = start; i < last; i++) {
store(i, make_freelist_entry(i + 1));
uint32_t next_entry = i + 1;
store(i, make_freelist_entry(next_entry, current_freelist_size++));
}
store(last, make_freelist_entry(0));
store(last, make_freelist_entry(0, current_freelist_size));
// This must be a release store to prevent reordering of the preceeding
// stores to the freelist from being reordered past this store. See
......
This diff is collapsed.
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