blob: 64525528db3cbba82e5c84aa4fed1682e1426495 [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "wtf/PartitionAlloc.h"
#include <string.h>
#ifndef NDEBUG
#include <stdio.h>
#endif
COMPILE_ASSERT(WTF::kPartitionPageSize * 4 <= WTF::kSuperPageSize, ok_partition_page_size);
COMPILE_ASSERT(!(WTF::kSuperPageSize % WTF::kPartitionPageSize), ok_partition_page_multiple);
// Four sub-partition pages gives us room to hack out a still-guard-paged piece // of metadata in the middle of a guard partition page.
COMPILE_ASSERT(WTF::kSubPartitionPageSize * 4 <= WTF::kPartitionPageSize, ok_sub_partition_page_size);
COMPILE_ASSERT(!(WTF::kPartitionPageSize % WTF::kSubPartitionPageSize), ok_sub_partition_page_multiple);
COMPILE_ASSERT(sizeof(WTF::PartitionPageHeader) <= WTF::kPartitionPageHeaderSize, PartitionPageHeader_not_too_big);
COMPILE_ASSERT(!(WTF::kPartitionPageHeaderSize % sizeof(void*)), PartitionPageHeader_size_should_be_multiple_of_pointer_size);
namespace WTF {
static size_t partitionBucketPageSize(size_t size)
{
double bestWasteRatio = 1.0f;
size_t bestPages = 0;
for (size_t i = kNumSubPagesPerPartitionPage - 1; i <= kNumSubPagesPerPartitionPage; ++i) {
size_t pageSize = kSubPartitionPageSize * i;
size_t numSlots = (pageSize - kPartitionPageHeaderSize) / size;
size_t waste = pageSize - (numSlots * size);
// Leave a page unfaulted is not free; the page will occupy an empty page table entry.
// Make a simple attempt to account for that.
waste += sizeof(void*) * (kNumSubPagesPerPartitionPage - i);
double wasteRatio = (double) waste / (double) pageSize;
if (wasteRatio < bestWasteRatio) {
bestWasteRatio = wasteRatio;
bestPages = i;
}
}
ASSERT(bestPages > 0);
return bestPages * kSubPartitionPageSize;
}
WTF_EXPORT void partitionAllocInit(PartitionRoot* root, size_t numBuckets, size_t maxAllocation)
{
ASSERT(!root->initialized);
root->initialized = true;
root->lock = 0;
root->totalSizeOfSuperPages = 0;
root->numBuckets = numBuckets;
root->maxAllocation = maxAllocation;
size_t i;
for (i = 0; i < root->numBuckets; ++i) {
PartitionBucket* bucket = &root->buckets()[i];
bucket->root = root;
bucket->currPage = &root->seedPage;
bucket->freePages = 0;
bucket->numFullPages = 0;
size_t size = partitionBucketSize(bucket);
bucket->pageSize = partitionBucketPageSize(size);
}
root->nextSuperPage = 0;
root->nextPartitionPage = 0;
root->nextPartitionPageEnd = 0;
root->currentExtent = &root->firstExtent;
root->firstExtent.superPageBase = 0;
root->firstExtent.superPagesEnd = 0;
root->firstExtent.next = 0;
root->seedPage.guard = reinterpret_cast<size_t*>(&root->seedPage.guard);
root->seedPage.numAllocatedSlots = 0;
root->seedPage.numUnprovisionedSlots = 0;
root->seedPage.bucket = &root->seedBucket;
root->seedPage.freelistHead = 0;
root->seedPage.next = &root->seedPage;
root->seedPage.prev = &root->seedPage;
root->seedBucket.root = root;
root->seedBucket.currPage = 0;
root->seedBucket.freePages = 0;
root->seedBucket.numFullPages = 0;
}
static bool partitionAllocShutdownBucket(PartitionBucket* bucket)
{
// Failure here indicates a memory leak.
bool noLeaks = !bucket->numFullPages;
PartitionFreepagelistEntry* entry = bucket->freePages;
while (entry) {
PartitionFreepagelistEntry* next = entry->next;
partitionFree(entry);
entry = next;
}
PartitionPageHeader* page = bucket->currPage;
do {
if (page->numAllocatedSlots)
noLeaks = false;
page = page->next;
} while (page != bucket->currPage);
return noLeaks;
}
bool partitionAllocShutdown(PartitionRoot* root)
{
bool noLeaks = true;
ASSERT(root->initialized);
root->initialized = false;
size_t i;
// First, free the non-metadata buckets. Freeing the free pages in these
// buckets will depend on the metadata bucket. There's no need to free or
// examine the metadata bucket because we now track super pages separately.
for (i = 0; i < root->numBuckets; ++i) {
if (i != kInternalMetadataBucket) {
PartitionBucket* bucket = &root->buckets()[i];
if (!partitionAllocShutdownBucket(bucket))
noLeaks = false;
}
}
// Now that we've examined all partition pages in all buckets, it's safe
// to free all our super pages. We first collect the super page pointers
// on the stack because some of them are themselves store in super pages.
char* superPages[kMaxPartitionSize / kSuperPageSize];
size_t numSuperPages = 0;
PartitionSuperPageExtentEntry* entry = &root->firstExtent;
while (entry) {
char* superPage = entry->superPageBase;
while (superPage != entry->superPagesEnd) {
superPages[numSuperPages] = superPage;
numSuperPages++;
superPage += kSuperPageSize;
}
entry = entry->next;
}
ASSERT(numSuperPages == root->totalSizeOfSuperPages / kSuperPageSize);
for (size_t i = 0; i < numSuperPages; ++i)
freeSuperPages(superPages[i], kSuperPageSize);
return noLeaks;
}
static ALWAYS_INLINE PartitionPageHeader* partitionAllocPage(PartitionRoot* root)
{
if (LIKELY(root->nextPartitionPage != 0)) {
// In this case, we can still hand out pages from a previous
// super page allocation.
char* ret = root->nextPartitionPage;
root->nextPartitionPage += kPartitionPageSize;
if (UNLIKELY(root->nextPartitionPage == root->nextPartitionPageEnd)) {
// We ran out, need to get more pages next time.
root->nextPartitionPage = 0;
root->nextPartitionPageEnd = 0;
}
return reinterpret_cast<PartitionPageHeader*>(ret);
}
// Need a new super page.
root->totalSizeOfSuperPages += kSuperPageSize;
RELEASE_ASSERT(root->totalSizeOfSuperPages <= kMaxPartitionSize);
// We need to put a guard page in front if either:
// a) This is the first super page allocation.
// b) The super page did not end up at our suggested address.
bool needsGuard = false;
if (UNLIKELY(root->nextSuperPage == 0)) {
needsGuard = true;
root->nextSuperPage = getRandomSuperPageBase();
}
char* superPage = reinterpret_cast<char*>(allocSuperPages(root->nextSuperPage, kSuperPageSize));
char* ret = superPage;
if (superPage != root->nextSuperPage) {
needsGuard = true;
// Re-randomize the base location for next time just in case the
// underlying operating system picks lousy locations for mappings.
root->nextSuperPage = 0;
} else {
root->nextSuperPage = superPage + kSuperPageSize;
}
root->nextPartitionPageEnd = superPage + kSuperPageSize;
if (needsGuard) {
// Leave a writeable hole in the middle of the guard partition page.
// We'll only fault it if we need to create a new super page extent, in which case it will used to maintain a singly linked list of super page extents.
setSystemPagesInaccessible(superPage, kSubPartitionPageSize);
setSystemPagesInaccessible(superPage + (kPartitionPageSize - kSubPartitionPageSize), kSubPartitionPageSize);
ret += kPartitionPageSize;
}
root->nextPartitionPage = ret + kPartitionPageSize;
// We allocated a new super page so update super page metadata.
// First check if this is a new extent or not.
PartitionSuperPageExtentEntry* currentExtent = root->currentExtent;
if (UNLIKELY(needsGuard)) {
if (currentExtent->superPageBase) {
// We already have a super page, so need to allocate metadata in the linked list.
PartitionSuperPageExtentEntry* newEntry = reinterpret_cast<PartitionSuperPageExtentEntry*>(currentExtent->superPageBase + kSubPartitionPageSize);
newEntry->next = 0;
currentExtent->next = newEntry;
currentExtent = newEntry;
root->currentExtent = newEntry;
}
currentExtent->superPageBase = superPage;
currentExtent->superPagesEnd = superPage + kSuperPageSize;
} else {
// We allocated next to an existing extent so just nudge the size up a little.
currentExtent->superPagesEnd += kSuperPageSize;
ASSERT(ret >= currentExtent->superPageBase && ret < currentExtent->superPagesEnd);
}
return reinterpret_cast<PartitionPageHeader*>(ret);
}
static ALWAYS_INLINE void partitionUnusePage(PartitionPageHeader* page)
{
decommitSystemPages(page, kPartitionPageSize);
}
static ALWAYS_INLINE size_t partitionBucketSlots(const PartitionBucket* bucket)
{
return (bucket->pageSize - kPartitionPageHeaderSize) / partitionBucketSize(bucket);
}
static ALWAYS_INLINE void partitionPageReset(PartitionPageHeader* page, PartitionBucket* bucket)
{
ASSERT(page != &bucket->root->seedPage);
page->guard = reinterpret_cast<size_t*>(&page->guard);
page->numAllocatedSlots = 0;
page->numUnprovisionedSlots = partitionBucketSlots(bucket);
ASSERT(page->numUnprovisionedSlots > 1);
page->bucket = bucket;
// NULLing the freelist is not strictly necessary but it makes an ASSERT in partitionPageFillFreelist simpler.
page->freelistHead = 0;
}
static ALWAYS_INLINE char* partitionPageAllocAndFillFreelist(PartitionPageHeader* page)
{
ASSERT(page != &page->bucket->root->seedPage);
size_t numSlots = page->numUnprovisionedSlots;
ASSERT(numSlots);
PartitionBucket* bucket = page->bucket;
// We should only get here when _every_ slot is either used or unprovisioned.
// (The third state is "on the freelist". If we have a non-empty freelist, we should not get here.)
ASSERT(numSlots + page->numAllocatedSlots == partitionBucketSlots(bucket));
// Similarly, make explicitly sure that the freelist is empty.
ASSERT(!page->freelistHead);
size_t size = partitionBucketSize(bucket);
char* base = reinterpret_cast<char*>(page);
char* returnObject = base + kPartitionPageHeaderSize + (size * page->numAllocatedSlots);
char* nextFreeObject = returnObject + size;
char* subPageLimit = reinterpret_cast<char*>((reinterpret_cast<uintptr_t>(returnObject) + kSubPartitionPageMask) & ~kSubPartitionPageMask);
size_t numNewFreelistEntries = 0;
if (LIKELY(subPageLimit > nextFreeObject))
numNewFreelistEntries = (subPageLimit - nextFreeObject) / size;
// We always return an object slot -- that's the +1 below.
// We do not neccessarily create any new freelist entries, because we cross sub page boundaries frequently for large bucket sizes.
numSlots -= (numNewFreelistEntries + 1);
page->numUnprovisionedSlots = numSlots;
page->numAllocatedSlots++;
if (LIKELY(numNewFreelistEntries)) {
PartitionFreelistEntry* entry = reinterpret_cast<PartitionFreelistEntry*>(nextFreeObject);
page->freelistHead = entry;
while (--numNewFreelistEntries) {
nextFreeObject += size;
PartitionFreelistEntry* nextEntry = reinterpret_cast<PartitionFreelistEntry*>(nextFreeObject);
entry->next = partitionFreelistMask(nextEntry);
entry = nextEntry;
}
entry->next = partitionFreelistMask(0);
} else {
page->freelistHead = 0;
}
return returnObject;
}
static ALWAYS_INLINE void partitionUnlinkPage(PartitionPageHeader* page)
{
ASSERT(page != &page->bucket->root->seedPage);
ASSERT(page->prev->next == page);
ASSERT(page->next->prev == page);
page->next->prev = page->prev;
page->prev->next = page->next;
}
static ALWAYS_INLINE void partitionLinkPageBefore(PartitionPageHeader* newPage, PartitionPageHeader* nextPage)
{
ASSERT(nextPage != &nextPage->bucket->root->seedPage);
ASSERT(nextPage->prev->next == nextPage);
ASSERT(nextPage->next->prev == nextPage);
newPage->next = nextPage;
newPage->prev = nextPage->prev;
nextPage->prev->next = newPage;
nextPage->prev = newPage;
}
void* partitionAllocSlowPath(PartitionBucket* bucket)
{
// The slow path is called when the freelist is empty.
PartitionPageHeader* page = bucket->currPage;
PartitionPageHeader* next = page->next;
ASSERT(page == &bucket->root->seedPage || (page->bucket == bucket && next->bucket == bucket));
// First, see if the current partition page still has capacity and if so,
// fill out the freelist a little more.
if (LIKELY(page->numUnprovisionedSlots))
return partitionPageAllocAndFillFreelist(page);
// Second, look for a page in our linked ring list of non-full pages.
while (LIKELY(next != page)) {
ASSERT(next->bucket == bucket);
ASSERT(next->next->prev == next);
ASSERT(next->prev->next == next);
ASSERT(next != &bucket->root->seedPage);
partitionValidatePage(next);
if (LIKELY(next->freelistHead != 0)) {
bucket->currPage = next;
PartitionFreelistEntry* ret = next->freelistHead;
next->freelistHead = partitionFreelistMask(ret->next);
next->numAllocatedSlots++;
return ret;
}
if (LIKELY(next->numUnprovisionedSlots)) {
bucket->currPage = next;
return partitionPageAllocAndFillFreelist(next);
}
// Pull this page out of the non-full page list, since it has no free
// slots.
// This tags the page as full so that free'ing can tell, and move
// the page back into the non-full page list when appropriate.
ASSERT(next->numAllocatedSlots == partitionBucketSlots(bucket));
next->numAllocatedSlots = -next->numAllocatedSlots;
partitionUnlinkPage(next);
++bucket->numFullPages;
next = next->next;
}
// After we've considered and rejected every partition page in the list,
// we should by definition have a single self-linked page left. We will
// replace this single page with the new page we choose.
ASSERT(page == page->next);
ASSERT(page == page->prev);
ASSERT(page == &bucket->root->seedPage || page->numAllocatedSlots == partitionBucketSlots(bucket));
if (LIKELY(page != &bucket->root->seedPage)) {
page->numAllocatedSlots = -page->numAllocatedSlots;
++bucket->numFullPages;
}
// Third, look in our list of freed but reserved pages.
PartitionPageHeader* newPage;
PartitionFreepagelistEntry* pagelist = bucket->freePages;
if (LIKELY(pagelist != 0)) {
ASSERT(page != &bucket->root->seedPage);
newPage = pagelist->page;
bucket->freePages = pagelist->next;
} else {
// Fourth. If we get here, we need a brand new page.
newPage = partitionAllocPage(bucket->root);
}
newPage->prev = newPage;
newPage->next = newPage;
bucket->currPage = newPage;
partitionPageReset(newPage, bucket);
char* ret = partitionPageAllocAndFillFreelist(newPage);
// We avoid re-entrancy concerns by freeing any free page list entry last,
// once all the operations of the actual allocation are complete.
if (LIKELY(pagelist != 0))
partitionFree(pagelist);
return ret;
}
void partitionFreeSlowPath(PartitionPageHeader* page)
{
PartitionBucket* bucket = page->bucket;
ASSERT(page != &bucket->root->seedPage);
ASSERT(bucket->currPage != &bucket->root->seedPage);
partitionValidatePage(bucket->currPage);
if (LIKELY(page->numAllocatedSlots == 0)) {
// Page became fully unused.
// If it's the current page, change it!
if (LIKELY(page == bucket->currPage)) {
if (UNLIKELY(page->next == page)) {
// We do not free the last active page in a bucket.
// To do so is a serious performance issue as we can get into
// a repeating state change between 0 and 1 objects of a given
// size allocated; and each state change incurs either a system
// call or a page fault, or more.
return;
}
bucket->currPage = page->next;
}
partitionUnlinkPage(page);
partitionUnusePage(page);
PartitionFreepagelistEntry* entry = static_cast<PartitionFreepagelistEntry*>(partitionBucketAlloc(&bucket->root->buckets()[kInternalMetadataBucket]));
entry->page = page;
entry->next = bucket->freePages;
bucket->freePages = entry;
} else {
// Ensure that the page is full. That's the only valid case if we
// arrive here.
ASSERT(page->numAllocatedSlots < 0);
// Fully used page became partially used. It must be put back on the
// non-full page list. Also make it the current page to increase the
// chances of it being filled up again. The old current page will be
// the next page.
partitionLinkPageBefore(page, bucket->currPage);
bucket->currPage = page;
page->numAllocatedSlots = -page->numAllocatedSlots - 2;
ASSERT(page->numAllocatedSlots == partitionBucketSlots(bucket) - 1);
--bucket->numFullPages;
}
}
void* partitionReallocGeneric(PartitionRoot* root, void* ptr, size_t newSize)
{
#if defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
return realloc(ptr, newSize);
#else
size_t oldIndex;
if (LIKELY(partitionPointerIsValid(root, ptr))) {
void* realPtr = partitionCookieFreePointerAdjust(ptr);
PartitionBucket* bucket = partitionPointerToPage(realPtr)->bucket;
ASSERT(bucket->root == root);
oldIndex = bucket - root->buckets();
} else {
oldIndex = root->numBuckets;
}
size_t allocSize = QuantizedAllocation::quantizedSize(newSize);
allocSize = partitionCookieSizeAdjust(allocSize);
size_t newIndex = allocSize >> kBucketShift;
if (newIndex > root->numBuckets)
newIndex = root->numBuckets;
if (oldIndex == newIndex) {
// Same bucket. But kNumBuckets indicates the fastMalloc "bucket" so in
// that case we're not done; we have to forward to fastRealloc.
if (oldIndex == root->numBuckets)
return WTF::fastRealloc(ptr, newSize);
return ptr;
}
// This realloc cannot be resized in-place. Sadness.
void* ret = partitionAllocGeneric(root, newSize);
size_t oldSize = oldIndex << kBucketShift;
size_t copySize = oldSize;
if (newSize < oldSize)
copySize = newSize;
memcpy(ret, ptr, copySize);
partitionFreeGeneric(root, ptr);
return ret;
#endif
}
#ifndef NDEBUG
void partitionDumpStats(const PartitionRoot& root)
{
size_t i;
size_t totalLive = 0;
size_t totalResident = 0;
size_t totalFreeable = 0;
for (i = 0; i < root.numBuckets; ++i) {
const PartitionBucket& bucket = root.buckets()[i];
if (bucket.currPage == &bucket.root->seedPage && !bucket.freePages && !bucket.numFullPages) {
// Empty bucket with no freelist or full pages. Skip reporting it.
continue;
}
size_t numFreePages = 0;
const PartitionFreepagelistEntry* freePages = bucket.freePages;
while (freePages) {
++numFreePages;
freePages = freePages->next;
}
size_t bucketSlotSize = partitionBucketSize(&bucket);
size_t bucketNumSlots = partitionBucketSlots(&bucket);
size_t bucketUsefulStorage = bucketSlotSize * bucketNumSlots;
size_t bucketWaste = bucket.pageSize - bucketUsefulStorage;
size_t numActiveBytes = bucket.numFullPages * bucketUsefulStorage;
size_t numResidentBytes = bucket.numFullPages * bucket.pageSize;
size_t numFreeableBytes = 0;
size_t numActivePages = 0;
const PartitionPageHeader* page = bucket.currPage;
do {
if (page != &bucket.root->seedPage) {
++numActivePages;
numActiveBytes += (page->numAllocatedSlots * bucketSlotSize);
size_t pageBytesResident = ((bucketNumSlots - page->numUnprovisionedSlots) * bucketSlotSize) + kPartitionPageHeaderSize;
// Round up to sub page size.
pageBytesResident = (pageBytesResident + kSubPartitionPageMask) & ~kSubPartitionPageMask;
numResidentBytes += pageBytesResident;
if (!page->numAllocatedSlots)
numFreeableBytes += pageBytesResident;
}
page = page->next;
} while (page != bucket.currPage);
totalLive += numActiveBytes;
totalResident += numResidentBytes;
totalFreeable += numFreeableBytes;
printf("bucket size %ld (pageSize %ld waste %ld): %ld alloc/%ld commit/%ld freeable bytes, %ld/%ld/%ld full/active/free pages\n", bucketSlotSize, static_cast<size_t>(bucket.pageSize), bucketWaste, numActiveBytes, numResidentBytes, numFreeableBytes, static_cast<size_t>(bucket.numFullPages), numActivePages, numFreePages);
}
printf("total live: %ld bytes\n", totalLive);
printf("total resident: %ld bytes\n", totalResident);
printf("total freeable: %ld bytes\n", totalFreeable);
fflush(stdout);
}
#endif // !NDEBUG
} // namespace WTF