blob: 86d25cc978d538e64e7c4257b44c8f32544500f2 [file] [log] [blame]
/*-------------------------------------------------------------------------
*
* sharedcache_gclock.c
* A generalized clock replacement policy for the shared cache
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "utils/sharedcache.h"
#include "utils/syncrefhashtable.h"
#include "utils/atomic.h"
/* Replacement policy default parameters */
/* Number of clock loops before giving up on finding a victim */
#define CACHE_DEFAULT_CLOCK_MAX_LOOPS 100
/* Number of entries to advance clock on look-up */
#define CACHE_DEFAULT_ENTRIES_ADVANCE 50
/* Amount to decrement utility of an entry at clock advance */
#define CACHE_DEFAULT_UTILITY_DECREMENT 7
/* Amount to increment utility of an entry when used */
#define CACHE_DEFAULT_UTILITY_INCREMENT 500
/*
* Initialize parameters required for the replacement policy to run
*/
void
Cache_InitReplacementPolicy(Cache *cache)
{
cache->cacheHdr->policyContext.clockPointer = 0;
cache->cacheHdr->policyContext.maxClockLoops = CACHE_DEFAULT_CLOCK_MAX_LOOPS;
cache->cacheHdr->policyContext.entriesAdvance = CACHE_DEFAULT_ENTRIES_ADVANCE;
cache->cacheHdr->policyContext.utilityDecrement = CACHE_DEFAULT_UTILITY_DECREMENT;
cache->cacheHdr->policyContext.utilityIncrement = CACHE_DEFAULT_UTILITY_INCREMENT;
}
/*
* Atomically advance clock hand by one position, wrapping around
* when reaching end of the array.
*
* Sets wraparound to true if clock hand wraps around
*
* Returns the new clock hand index
*/
static int32
Cache_NextClockHand(Cache *cache, bool *wraparound)
{
Assert(NULL != cache);
Assert(NULL != wraparound);
int32 oldIndex = cache->cacheHdr->policyContext.clockPointer;
int32 newIndex = gp_atomic_incmod_32(&cache->cacheHdr->policyContext.clockPointer, cache->cacheHdr->nEntries);
Assert(newIndex < cache->cacheHdr->nEntries);
*wraparound = (newIndex < oldIndex);
return newIndex;
}
/*
* Advance cache clock by a set number of entries and decrement each entry's
* utility by decAmount.
*
* This function doesn't do any look-ups or locking, it's supposed to be fast.
*/
void
Cache_AdvanceClock(Cache *cache)
{
Assert(NULL != cache);
long entriesTouched = 0;
while (entriesTouched++ < cache->cacheHdr->policyContext.entriesAdvance)
{
bool wraparound = false;
int crtIndex = Cache_NextClockHand(cache, &wraparound);
CacheEntry *crtEntry = Cache_GetEntryByIndex(cache->cacheHdr, crtIndex);
gp_atomic_dec_positive_32(&crtEntry->utility,
cache->cacheHdr->policyContext.utilityDecrement);
}
}
/*
* Run cache eviction algorithm
*
* It will try to evict enough entries to add up to evictSize. Returns the
* actual accumulated size of the entries evicted
*/
int64
Cache_Evict(Cache *cache, int64 evictRequestSize)
{
Assert(NULL != cache);
Assert(evictRequestSize > 0);
Cache_TimedOperationStart();
int64 evictedSize = 0;
uint32 unsuccessfulLoops = 0;
bool foundVictim = false;
uint32 decAmount = cache->cacheHdr->policyContext.utilityDecrement;
Cache_Stats *cacheStats = &cache->cacheHdr->cacheStats;
while (true)
{
bool wraparound = false;
int32 entryIdx = Cache_NextClockHand(cache, &wraparound);
Assert(entryIdx < cache->cacheHdr->nEntries);
Cache_UpdatePerfCounter(&cacheStats->noEntriesScanned,1 /* delta */);
if (wraparound)
{
unsuccessfulLoops++;
Cache_UpdatePerfCounter(&cacheStats->noWraparound, 1 /* delta */);
if (!foundVictim)
{
/*
* We looped around and did not manage to evict any entries.
* Double the amount we decrement eviction candidate's utility by.
* This makes the eviction algorithm look for a victim more aggressively
*/
if (decAmount <= CACHE_MAX_UTILITY / 2)
{
decAmount = 2 * decAmount;
}
else
{
decAmount = CACHE_MAX_UTILITY;
}
}
foundVictim = false;
if (unsuccessfulLoops > cache->cacheHdr->policyContext.maxClockLoops)
{
/* Can't find any cached and unused entries candidates for evictions, even after looping around
* maxClockLoops times. Give up looking for victims. */
Cache_TimedOperationRecord(&cacheStats->timeEvictions, &cacheStats->maxTimeEvict);
break;
}
}
CacheEntry *crtEntry = Cache_GetEntryByIndex(cache->cacheHdr, entryIdx);
if (crtEntry->state != CACHE_ENTRY_CACHED)
{
/* Not interested in free/acquired/deleted entries. Go back and advance clock hand */
continue;
}
CacheAnchor *anchor = (CacheAnchor *) SyncHTLookup(cache->syncHashtable, &crtEntry->hashvalue);
if (NULL == anchor)
{
/* There's no anchor for this entry, someone might have snatched it in the meantime */
continue;
}
SpinLockAcquire(&anchor->spinlock);
if (crtEntry->state != CACHE_ENTRY_CACHED)
{
/* Someone freed this entry in the meantime, before we got a chance to acquire the anchor lock */
SpinLockRelease(&anchor->spinlock);
SyncHTRelease(cache->syncHashtable, (void *) anchor);
continue;
}
/* Ok, did all the checks, this entry must be valid now */
CACHE_ASSERT_VALID(crtEntry);
if (crtEntry->pinCount > 0)
{
/* Entry is in use and can't be evicted. Go back and advance clock hand */
SpinLockRelease(&anchor->spinlock);
SyncHTRelease(cache->syncHashtable, (void *) anchor);
continue;
}
/* Decrement utility */
gp_atomic_dec_positive_32(&crtEntry->utility, decAmount);
/* Just decremented someone's utility. Reset our unsuccessful loops counter */
unsuccessfulLoops = 0;
if (crtEntry->utility > 0)
{
/* Entry has non-zero utility, we shouldn't evict it. Go back and advance clock hand */
SpinLockRelease(&anchor->spinlock);
SyncHTRelease(cache->syncHashtable, (void *) anchor);
continue;
}
/* Found our victim */
Assert(0 == crtEntry->pinCount);
CACHE_ASSERT_VALID(crtEntry);
Assert(crtEntry->utility == 0);
#if USE_ASSERT_CHECKING
int32 casResult =
#endif
compare_and_swap_32(&crtEntry->state, CACHE_ENTRY_CACHED, CACHE_ENTRY_DELETED);
Assert(1 == casResult);
SpinLockRelease(&anchor->spinlock);
foundVictim = true;
evictedSize += crtEntry->size;
/* Don't update noFreeEntries yet. It will be done in Cache_AddToFreelist */
Cache_UpdatePerfCounter(&cacheStats->noCachedEntries, -1 /* delta */);
/* Unlink entry from the anchor chain */
SpinLockAcquire(&anchor->spinlock);
Cache_UnlinkEntry(cache, anchor, crtEntry);
SpinLockRelease(&anchor->spinlock);
SyncHTRelease(cache->syncHashtable, (void *) anchor);
if (NULL != cache->cleanupEntry)
{
/* Call client-side cleanup for entry */
cache->cleanupEntry(CACHE_ENTRY_PAYLOAD(crtEntry));
}
Cache_LockEntry(cache, crtEntry);
Assert(crtEntry->state == CACHE_ENTRY_DELETED);
crtEntry->state = CACHE_ENTRY_FREE;
#if USE_ASSERT_CHECKING
Cache_MemsetPayload(cache, crtEntry);
#endif
Cache_UnlockEntry(cache, crtEntry);
Cache_AddToFreelist(cache, crtEntry);
Cache_UpdatePerfCounter(&cacheStats->noEvicts, 1 /* delta */);
Cache_TimedOperationRecord(&cacheStats->timeEvictions, &cacheStats->maxTimeEvict);
if (evictedSize >= evictRequestSize)
{
/* We evicted as much as requested */
break;
}
Cache_TimedOperationStart();
}
return evictedSize;
}
/*
* Marks an entry as recently used when it is accessed. Atomically
* increments the utility of the entry.
*/
void
Cache_TouchEntry(Cache *cache, CacheEntry *entry)
{
Assert(NULL != cache);
Assert(NULL != entry);
Assert(entry->pinCount > 0);
gp_atomic_inc_ceiling_32(&entry->utility,
cache->cacheHdr->policyContext.utilityIncrement,
CACHE_MAX_UTILITY);
}