From 5d07984c5d48d6253ea5884d69da3f06cdc90f1b Mon Sep 17 00:00:00 2001 From: antirez Date: Fri, 15 Jul 2016 12:12:52 +0200 Subject: [PATCH] LFU: Redis object level implementation. Implementation of LFU maxmemory policy for anything related to Redis objects. Still no actual eviction implemented. --- src/config.c | 2 ++ src/db.c | 8 ++++- src/evict.c | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/object.c | 24 ++++++++++---- src/server.h | 30 ++++++++++++----- 5 files changed, 142 insertions(+), 15 deletions(-) diff --git a/src/config.c b/src/config.c index 683ec8719..f443ce22a 100644 --- a/src/config.c +++ b/src/config.c @@ -45,9 +45,11 @@ typedef struct configEnum { configEnum maxmemory_policy_enum[] = { {"volatile-lru", MAXMEMORY_VOLATILE_LRU}, + {"volatile-lfu", MAXMEMORY_VOLATILE_LFU}, {"volatile-random",MAXMEMORY_VOLATILE_RANDOM}, {"volatile-ttl",MAXMEMORY_VOLATILE_TTL}, {"allkeys-lru",MAXMEMORY_ALLKEYS_LRU}, + {"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU}, {"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM}, {"noeviction",MAXMEMORY_NO_EVICTION}, {NULL, 0} diff --git a/src/db.c b/src/db.c index 03615fd34..b00227d81 100644 --- a/src/db.c +++ b/src/db.c @@ -53,7 +53,13 @@ robj *lookupKey(redisDb *db, robj *key, int flags) { server.aof_child_pid == -1 && !(flags & LOOKUP_NOTOUCH)) { - val->lru = LRU_CLOCK(); + if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { + unsigned long ldt = val->lru >> 8; + unsigned long counter = LFULogIncr(val->lru & 255); + val->lru = (ldt << 8) | counter; + } else { + val->lru = LRU_CLOCK(); + } } return val; } else { diff --git a/src/evict.c b/src/evict.c index 6ce3aef0d..753a2ac81 100644 --- a/src/evict.c +++ b/src/evict.c @@ -214,6 +214,99 @@ void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evic } } +/* ---------------------------------------------------------------------------- + * LFU (Least Frequently Used) implementation. + + * We have 24 total bits of space in each object in order to implement + * an LFU (Least Frequently Used) eviction policy, since we re-use the + * LRU field for this purpose. + * + * We split the 24 bits into two fields: + * + * 16 bits 8 bits + * +----------------+--------+ + * + Last decr time | LOG_C | + * +----------------+--------+ + * + * LOG_C is a logarithmic counter that provides an indication of the access + * frequency. However this field must also be decremented otherwise what used + * to be a frequently accessed key in the past, will remain ranked like that + * forever, while we want the algorithm to adapt to access pattern changes. + * + * So the remaining 16 bits are used in order to store the "decrement time", + * a reduced-precision Unix time (we take 16 bits of the time converted + * in minutes since we don't care about wrapping around) where the LOG_C + * counter is halved if it has an high value, or just decremented if it + * has a low value. + * + * New keys don't start at zero, in order to have the ability to collect + * some accesses before being trashed away, so they start at COUNTER_INIT_VAL. + * The logarithmic increment performed on LOG_C takes care of COUNTER_INIT_VAL + * when incrementing the key, so that keys starting at COUNTER_INIT_VAL + * (or having a smaller value) have a very high chance of being incremented + * on access. + * + * During decrement, the value of the logarithmic counter is halved if + * its current value is greater than two times the COUNTER_INIT_VAL, otherwise + * it is just decremented by one. + * --------------------------------------------------------------------------*/ + +/* Return the current time in minutes, just taking the least significant + * 16 bits. The returned time is suitable to be stored as LDT (last decrement + * time) for the LFU implementation. */ +unsigned long LFUGetTimeInMinutes(void) { + return (server.unixtime/60) & 65535; +} + +/* Given an object last decrement time, compute the minimum number of minutes + * that elapsed since the last decrement. Handle overflow (ldt greater than + * the current 16 bits minutes time) considering the time as wrapping + * exactly once. */ +unsigned long LFUTimeElapsed(unsigned long ldt) { + unsigned long now = LFUGetTimeInMinutes(); + if (now > ldt) return now-ldt; + return 65535-ldt+now; +} + +/* Logarithmically increment a counter. The greater is the current counter value + * the less likely is that it gets really implemented. Saturate it at 255. */ +#define LFU_LOG_FACTOR 10 +uint8_t LFULogIncr(uint8_t counter) { + if (counter == 255) return 255; + double r = (double)rand()/RAND_MAX; + double baseval = counter - LFU_INIT_VAL; + if (baseval < 0) baseval = 0; + double p = 1.0/(baseval*LFU_LOG_FACTOR+1); + if (r < p) counter++; + return counter; +} + +/* If the object decrement time is reached, decrement the LFU counter and + * update the decrement time field. Return the object frequency counter. + * + * This function is used in order to scan the dataset for the best object + * to fit: as we check for the candidate, we incrementally decrement the + * counter of the scanned objects if needed. */ +#define LFU_DECR_INTERVAL 1 +unsigned long LFUDecrAndReturn(robj *o) { + unsigned long ldt = o->lru >> 8; + unsigned long counter = o->lru & 255; + if (LFUTimeElapsed(ldt) > LFU_DECR_INTERVAL && counter) { + if (counter > LFU_INIT_VAL*2) { + counter /= 2; + } else { + counter--; + } + o->lru = (LFUGetTimeInMinutes()<<8) | counter; + } + return counter; +} + +/* ---------------------------------------------------------------------------- + * The external API for eviction: freeMemroyIfNeeded() is called by the + * server when there is data to add in order to make space if needed. + * --------------------------------------------------------------------------*/ + int freeMemoryIfNeeded(void) { size_t mem_reported, mem_used, mem_tofree, mem_freed; int slaves = listLength(server.slaves); diff --git a/src/object.c b/src/object.c index ec886f1f6..8f19f60f2 100644 --- a/src/object.c +++ b/src/object.c @@ -43,8 +43,13 @@ robj *createObject(int type, void *ptr) { o->ptr = ptr; o->refcount = 1; - /* Set the LRU to the current lruclock (minutes resolution). */ - o->lru = LRU_CLOCK(); + /* Set the LRU to the current lruclock (minutes resolution), or + * alternatively the LFU counter. */ + if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { + o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL; + } else { + o->lru = LRU_CLOCK(); + } return o; } @@ -82,7 +87,11 @@ robj *createEmbeddedStringObject(const char *ptr, size_t len) { o->encoding = OBJ_ENCODING_EMBSTR; o->ptr = sh+1; o->refcount = 1; - o->lru = LRU_CLOCK(); + if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { + o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL; + } else { + o->lru = LRU_CLOCK(); + } sh->len = len; sh->alloc = len; @@ -394,8 +403,7 @@ robj *tryObjectEncoding(robj *o) { * because every object needs to have a private LRU field for the LRU * algorithm to work well. */ if ((server.maxmemory == 0 || - (server.maxmemory_policy != MAXMEMORY_VOLATILE_LRU && - server.maxmemory_policy != MAXMEMORY_ALLKEYS_LRU)) && + !(server.maxmemory & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) && value >= 0 && value < OBJ_SHARED_INTEGERS) { @@ -715,8 +723,12 @@ void objectCommand(client *c) { if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk)) == NULL) return; addReplyLongLong(c,estimateObjectIdleTime(o)/1000); + } else if (!strcasecmp(c->argv[1]->ptr,"freq") && c->argc == 3) { + if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk)) + == NULL) return; + addReplyLongLong(c,o->lru&255); } else { - addReplyError(c,"Syntax error. Try OBJECT (refcount|encoding|idletime)"); + addReplyError(c,"Syntax error. Try OBJECT (refcount|encoding|idletime|freq)"); } } diff --git a/src/server.h b/src/server.h index 0354877fc..6a9fb2a09 100644 --- a/src/server.h +++ b/src/server.h @@ -341,13 +341,22 @@ typedef long long mstime_t; /* millisecond time type. */ #define SET_OP_DIFF 1 #define SET_OP_INTER 2 -/* Redis maxmemory strategies */ -#define MAXMEMORY_VOLATILE_LRU 0 -#define MAXMEMORY_VOLATILE_TTL 1 -#define MAXMEMORY_VOLATILE_RANDOM 2 -#define MAXMEMORY_ALLKEYS_LRU 3 -#define MAXMEMORY_ALLKEYS_RANDOM 4 -#define MAXMEMORY_NO_EVICTION 5 +/* Redis maxmemory strategies. Instead of using just incremental number + * for this defines, we use a set of flags so that testing for certain + * properties common to multiple policies is faster. */ +#define MAXMEMORY_FLAG_LRU (1<<0) +#define MAXMEMORY_FLAG_LFU (1<<1) +#define MAXMEMORY_FLAG_NO_SHARED_INTEGERS \ + (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) +#define MAXMEMORY_VOLATILE_LRU ((0<<8)|MAXMEMORY_FLAG_LRU) +#define MAXMEMORY_VOLATILE_LFU ((1<<8)|MAXMEMORY_FLAG_LFU) +#define MAXMEMORY_VOLATILE_TTL (2<<8) +#define MAXMEMORY_VOLATILE_RANDOM (3<<8) +#define MAXMEMORY_ALLKEYS_LRU ((4<<8)|MAXMEMORY_FLAG_LRU) +#define MAXMEMORY_ALLKEYS_LFU ((5<<8)|MAXMEMORY_FLAG_LFU) +#define MAXMEMORY_ALLKEYS_RANDOM (6<<8) +#define MAXMEMORY_NO_EVICTION (7<<8) + #define CONFIG_DEFAULT_MAXMEMORY_POLICY MAXMEMORY_NO_EVICTION /* Scripting */ @@ -528,7 +537,9 @@ typedef struct RedisModuleIO { typedef struct redisObject { unsigned type:4; unsigned encoding:4; - unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ + unsigned lru:LRU_BITS; /* LRU time (relative to server.lruclock) or + * LFU data (least significant 8 bits frequency + * and most significant 16 bits decreas time). */ int refcount; void *ptr; } robj; @@ -1606,6 +1617,9 @@ void activeExpireCycle(int type); /* evict.c -- maxmemory handling and LRU eviction. */ void evictionPoolAlloc(void); +#define LFU_INIT_VAL 5 +unsigned long LFUGetTimeInMinutes(void); +uint8_t LFULogIncr(uint8_t value); /* Git SHA1 */ char *redisGitSHA1(void);