*本文简述一个简单LRU缓存的c语言实现方式。
GitHub:https://github.com/Stand1210/c-LRU-
一·首先明确几个概念:
1.缓存的特点?
1⃣️:能在某种程度上降低访问数据的成本
2⃣️:其容量远小于外部存储的容量。
2.什么是LRU缓存?
LRU缓存是一种以LRU策略为缓存策略的缓存。
所谓缓存策略, 就是当缓存满了之后, 又有新数据需要加入内存时, 我们怎么从缓存中为新数据腾出空间的策略。这个权衡的过程就是所谓的缓存策略。
LRU, Least Recently Used的简写, 即近期最少使用算法。 该算法依据与程序的局部性原理, 其淘汰旧数据的策略是, 距离当前最久没有被访问过的数据应该被淘汰。
二·设计及实现
1⃣️
:
LRUCache接口:
//
// LRUCache.h
// LRU缓存接口
//
// Created by 宋珂琦 on 2017/4/13.
// Copyright © 2017年 宋珂琦. All rights reserved.
//
#ifndef LRUCache_h
#define LRUCache_h
/*创建LRU缓存*/
int LRUCacheCreate(int capacity, void **lruCache);
/*销毁LRU缓存*/
int LRUCacheDestory(void *lruCache);
/*将数据放入LRU缓存中*/
int LRUCacheSet(void *lruCache, char key, char data);
/*从缓存中获取数据*/
char LRUCacheGet(void *lruCache, char key);
/*打印缓存中的数据, 按访问时间从新到旧的顺序输出*/
void LRUCachePrint(void *lruCache);
#endif /* LRUCache_h */
上述接口中, 我们假设数据索引和LRU缓存的每个单位仅能存储一个char型的字符。
2⃣️:详细设计
- 使用双向链表维护LRU特性。
我们将在缓存中维护一个双向链表, 该链表奖缓存中的数据块按访问时间从新到旧排列起来:
1.当我们需要访问一块数据时, 我们先调用接口LRUCacheGet尝试从缓存中获取数据
2.如果缓存中刚好缓存了该数据, 那么我们将该数据从缓存的双向链表中摘除, 然后再重新放入到双向链表的表头
3.如果缓存中没有我们需要的数据, 那么我们从外部取得数据, 然后调用LRUCacheSet接口将该数据放入缓存, 此过程, 会将新数据插入到双向链表的表头
- 使用哈希表保证缓存中数据的访问速度
如果我们仅将缓存中的数据维护在一个链表中, 那么当我们需要从缓存中查找数据时, 旧意味着我们需要遍历链表。 这样的设计, 其时间复杂度为O(n). 是一种比较低效率的做法。 因此, 我们除了将数据维护在双向链表中, 我们同时还将数据维护在一个哈希表中。 哈希表访问数据的时间复杂度为O(1)。
//注意 在我们的哈希表的实现内部, 也维护了链表, 这是一种解决哈希冲突的方法。如图:
- 定义LRU缓存内部数据结构
依据上述设计, 一个LRU缓存包含了一个双向链表和一个哈希表, 因此我们可以这样定义我们LRU缓存的数据结构
/* 定义LRU缓存 */
typedef struct LRUCacheS{
int cacheCapacity; /*缓存的容量*/
cacheEntryS **hashMap; /*缓存的哈希表*/
cacheEntryS *lruListHead; /*缓存的双向链表表头*/
cacheEntryS *lruListTail; /*缓存的双向链表表尾*/
int lruListSize; /*缓存的双向链表节点个数*/
}LRUCacheS;
缓存中的单元, 既是一个双向链表节点, 同时又是哈希表当中的一个节点, 因此我们可以这样定义我们缓存单元的数据结构:
/*定义LRU缓存的缓存单元*/
typedef struct cacheEntryS{
char key; /* 数据的key */
char data; /* 数据的data */
struct cacheEntryS *hashListPrev; /* 缓存哈希表指针, 指向哈希链表的前一个元素 */
struct cacheEntryS *hashListNext; /* 缓存哈希表指针, 指向哈希链表的后一个元素 */
struct cacheEntryS *lruListPrev; /* 缓存双向链表指针, 指向链表的前一个元素 */
struct cacheEntryS *lruListNext; /* 缓存双向链表指针, 指向链表后一个元素 */
}cacheEntryS;
三·测试程序
我们以TDD(测试驱动开发)的方式来实现我们的LRU缓存, 在定义好接口后, 我们就可以先构造出我们的测试用例了:
//
// main.c
// LRU 缓存测试用例
//
// Created by 宋珂琦 on 2017/4/13.
// Copyright © 2017年 宋珂琦. All rights reserved.
//
#include <stdio.h>
#include <stdlib.h>
#include "LRUCache.h"
/*错误处理宏*/
#define HANDLE_ERROR(msg) \
do{ fprintf(stderr, "%s fail.\n", msg);exit(-1);}while(0);
/* 封装缓存数据存储接口, 此处我们让data同时充当key的角色 */
#define LRUCACHE_PUTDATA(cache, data) \
do {\
if (0 != LRUCacheSet(cache, data, data)) {\
fprintf(stderr, "put (%c %c) to cache fail. \n", data, data); \
}else {\
fprintf(stdout, "put (%c %c) to cache success.\n", data, data); \
} \
} while (0);
/* 封装缓存数据存储接口 */
#define LRUCACHE_GETDATA(cache, key) \
do {\
char data = LRUCacheGet(cache, key);\
if ('\0' == data) \
fprintf(stderr, "get data (Key: %c) from cache fail.\n", key);\
else if (key == data) \
fprintf(stdout, "get (%c, %c) from cache.\n", key, data);\
} while (0);
void testCase1(void)
{
fprintf(stdout, "=================================\n");
fprintf(stdout, "In testCase 1:\n");
void *LruCache;
if (0 != LRUCacheCreate(5, &LruCache)) {
HANDLE_ERROR("LRUCacheCreate");
}
LRUCACHE_PUTDATA(LruCache, 'A');
LRUCACHE_GETDATA(LruCache, 'A');
LRUCACHE_PUTDATA(LruCache, 'B');
LRUCACHE_GETDATA(LruCache, 'B');
LRUCACHE_PUTDATA(LruCache, 'C');
LRUCACHE_GETDATA(LruCache, 'C');
LRUCachePrint(LruCache);
LRUCACHE_PUTDATA(LruCache, 'D');
LRUCACHE_GETDATA(LruCache, 'D');
LRUCACHE_PUTDATA(LruCache, 'E');
LRUCACHE_GETDATA(LruCache, 'E');
LRUCACHE_PUTDATA(LruCache, 'A');
LRUCACHE_GETDATA(LruCache, 'A');
LRUCACHE_PUTDATA(LruCache, 'F');
LRUCACHE_GETDATA(LruCache, 'F');
LRUCachePrint(LruCache);
LRUCACHE_PUTDATA(LruCache, 'F');
LRUCACHE_GETDATA(LruCache, 'F');
LRUCachePrint(LruCache);
if (0 != LRUCacheDestory(LruCache))
HANDLE_ERROR("LRUCacheDestory");
fprintf(stdout, "In testCase 1 finished\n");
fprintf(stdout, "=================================\n");
}
void testCase2()
{
fprintf(stdout, "=================================\n");
fprintf(stdout, "In testCase 2:\n");
void *LruCache;
if (0 != LRUCacheCreate(3, &LruCache)) {
HANDLE_ERROR("LRUCacheCreate");
}
LRUCACHE_PUTDATA(LruCache, 'W');
LRUCACHE_PUTDATA(LruCache, 'X');
LRUCACHE_PUTDATA(LruCache, 'W');
LRUCACHE_PUTDATA(LruCache, 'Y');
LRUCACHE_PUTDATA(LruCache, 'Z');
LRUCachePrint(LruCache);
LRUCACHE_GETDATA(LruCache, 'Z');
LRUCACHE_GETDATA(LruCache, 'Y');
LRUCACHE_GETDATA(LruCache, 'W');
LRUCACHE_GETDATA(LruCache, 'X');
LRUCACHE_GETDATA(LruCache, 'W');
LRUCachePrint(LruCache);
/*YZWYX!*/
LRUCACHE_PUTDATA(LruCache, 'Y');
LRUCACHE_PUTDATA(LruCache, 'Z');
LRUCACHE_PUTDATA(LruCache, 'W');
LRUCACHE_PUTDATA(LruCache, 'Y');
LRUCACHE_PUTDATA(LruCache, 'X');
LRUCachePrint(LruCache);
LRUCACHE_GETDATA(LruCache, 'X');
LRUCACHE_GETDATA(LruCache, 'Y');
LRUCACHE_GETDATA(LruCache, 'W');
LRUCACHE_GETDATA(LruCache, 'Z');
LRUCACHE_GETDATA(LruCache, 'Y');
LRUCachePrint(LruCache); /*WYX*/
/*XYXY!*/
LRUCACHE_PUTDATA(LruCache, 'X');
LRUCACHE_PUTDATA(LruCache, 'Y');
LRUCACHE_PUTDATA(LruCache, 'X');
LRUCACHE_PUTDATA(LruCache, 'Y');
LRUCachePrint(LruCache);/*YX*/
LRUCACHE_GETDATA(LruCache, 'Y');
LRUCACHE_GETDATA(LruCache, 'X');
LRUCACHE_GETDATA(LruCache, 'Y');
LRUCACHE_GETDATA(LruCache, 'X');
LRUCachePrint(LruCache); /*XY*/
fprintf(stdout, "In testCase 2 finished\n");
fprintf(stdout, "=================================\n");
}
void testCase3()
{
fprintf(stdout, "=================================\n");
fprintf(stdout, "In testCase 3:\n");
void *LruCache;
if (0 != LRUCacheCreate(5, &LruCache)) {
HANDLE_ERROR("LRUCacheCreate");
}
LRUCACHE_PUTDATA(LruCache, 'E');
LRUCACHE_PUTDATA(LruCache, 'I');
LRUCACHE_PUTDATA(LruCache, 'E');
LRUCACHE_PUTDATA(LruCache, 'I');
LRUCACHE_PUTDATA(LruCache, 'O');
LRUCachePrint(LruCache);
LRUCACHE_GETDATA(LruCache, 'A');
LRUCACHE_GETDATA(LruCache, 'I');
LRUCACHE_GETDATA(LruCache, 'B');
LRUCACHE_GETDATA(LruCache, 'O');
LRUCACHE_GETDATA(LruCache, 'C');
LRUCACHE_PUTDATA(LruCache, 'E');
LRUCachePrint(LruCache);
fprintf(stdout, "In testCase 3 finished\n");
fprintf(stdout, "=================================\n");
}
int main()
{
testCase1();
testCase2();
testCase3();
return 0;
}
测试结果:
- 测试用例1
- 测试用例2
- 测试用例3