• Writing LRU Cache in C


    https://amitadhikari.com/posts/writing-lru-cache-in-c/

    https://github.com/adkri/lrucache/blob/master/lru.c

    https://www.andreinc.net/2021/10/02/implementing-hash-tables-in-c-part-1

    https://github.com/nomemory/chained-hash-table-c

    https://github.com/nomemory/open-adressing-hash-table-c

    Writing LRU Cache in C

    In this post we will discuss the process of creating a Least Recently Used(LRU) cache structure in C. The Least Recently Used policy is very common model to create caching structures in computers.

    The LRU cache eviction policy is as simple as it sounds. It describes the eviction strategy of data in a cache, in this case, if the cache requires to evict data it will evict the least recently used item.

    Why and when would we need such a structure?

    Cache

    First of all, let us define a cache. In computer science, a cache(pronounced “cash”) is simply a local data store we use to reduce data retrival time. While this concept might seem very simple, it is equally important in all levels of abstraction in a computer.

    For example, in the computer hardware level we can store terabytes of information in a hard drive. However, the speed of the hard drive is slow and cannot compare to the operations a cpu can do within the same timeframe, so we put the running application on the RAM. Now, the cpu can work with data in the RAM instead of the hard drive. In this case, we can say the RAM is a “cache” for the hard drive.

    Even in the software level, we have different caches. A cache to store the result of heavy computation, result of network calls, or data retrieved from the database.

    By the nature of caching, we can see infer that it exists because we have finite memory at every level of abstraction we work on. Then we know that if we build a cache we should work on the assumption of limited memory available to us. Thus, we have the need for a cache eviction policy. Here, we define eviction as the removal of entries from the cache after it cannot store any more elements.

    There are various different cache eviction strategies like First In First Out (FIFO), Least Recently Used (LRU), Most Frequently Used(MFU), or Random. Each one would be appropriate depending on requirements of the system.

    Datastructure

    For our LRU cache, let us define the interface for it and then the internal design specifications that is required.

    The cache itself should be very simple, supporting only the basic operations: get and put.

    // Interface for the cache
    void* get(char* key);
    void put(char* key, void* value);

    Since the requirement from cache arises to make a current system faster, the time complexity should not be anything else than constant.

    We see the cache interface resembles most closely to a hashtable structure found in most of computer science. So our implementation will include a hashtable with slight modifications. With a hashtable we have a basic cache, the ability to store and retrieve data. But to implement our eviction strategy, we need to remember the items that are being read and the items which are not being used. To perform such operations we implement a hashtable with a doubly linked list structure. The linked structure allows us to store data and move them around freely. This enables us to say, keep all the least used items at the tail of the list, while moving the used items to the front of the list. Then, we use hashtable to quickly jump around and get items from the list. Both of the operations can now be performed in constant time.

    Implementation of the LRU Cache:

    typedef struct Node {
        char* key;
        char* value;
        struct Node* next;
        struct Node* prev;
        struct Node* hashNext;
    } Node;
    
    typedef struct Table {
        int capacity;
        Node* *array;
    } Table;
    
    typedef struct List {
        int size;
        int capacity;
        Node* head;
        Node* tail;
    } List;
    
    typedef struct LRUCache {
        Table* table;
        List* list;
    } LRUCache;

    Now first lets make the methods to allocate space for these structures:

    Node* createNode(char* key, char* value) {
        Node* newNode = (Node*) malloc(sizeof(Node));
        newNode -> key = key;
        newNode -> value = value;
        newNode -> next = NULL;
        newNode -> prev = NULL;
        newNode -> hashNext = NULL;
        return newNode;
    }
    
    Table* createTable(int capacity) {
        Table* newhash = (Table*) malloc(sizeof(Table));
        newhash -> capacity = capacity;
        newhash -> array = (Node**) malloc(sizeof(Node) * capacity);
        for(size_t i =0 ;i < capacity; i++)
            newhash -> array[i] = NULL;
        return newhash;
    }
    
    List* createList(int capacity) {
        List* newList = (List*) malloc(sizeof(List));
        newList -> size = 0;
        newList -> capacity = capacity;
        newList -> head = newList -> tail = NULL;
        return newList;
    }
    
    LRUCache* createCache(int size) {
        LRUCache* newCache = (LRUCache*) malloc(sizeof(LRUCache));   
        Table* table = createTable(size * 2);
        List* list = createList(size);
        newCache -> table = table;
        newCache -> list = list;
        return newCache;
    }

    Now the function to add elements to our cache:

    void put(LRUCache* cache, char* key, char* value) {
        Node* valNode = createNode(key, value);
    
        if(addToHash(cache -> table, valNode)) // already in list
            moveToFront(cache, valNode -> key);    
        else
            addToList(cache, valNode); 
    }

    Here we first create a Node so we can pass the same reference to the other functions.

    We will call the addToHash function that add the entry to the cache. If the key already exists, it will update with new value and return 1, otherwise it adds to the hashtable and returns 0.

    If the key was already there, we need to move it to the front of list with moveToFront. Or if we added new entry to the hashtable we also add it to list with addToList.

    Now let’s write the function definitions:

    addToHash

    int addToHash(Table* table, Node* valNode) {
        size_t hashCode = getHashCode(table, valNode -> key);    
        if(table -> array[hashCode] != NULL) { // check if is in hash
    
            Node* curr = table->array[hashCode];
    
            while(curr -> hashNext != NULL) {
                if(strcmp(curr -> key, valNode -> key) == 0) {
                    curr -> value = valNode -> value;
                    return 1;
                } 
                curr = curr -> hashNext;
            }
            // last node
            if(strcmp(curr -> key, valNode -> key) == 0) {
                    curr -> value = valNode -> value;
                    return 1;
            } 
    
            // add after last node
            curr -> hashNext = valNode;
            return 0;
        } else {
            table -> array[hashCode] = valNode;
            return 0;
        }
    }

    moveToFront

    void moveToFront(LRUCache* cache, char* key) {
        Table* table = cache -> table;
        List* list = cache -> list;
    
        // return if only element in list
        if(list -> size == 1) 
            return;
    
        size_t hashCode = getHashCode(table, key);
        Node* curr = table -> array[hashCode];
    
        // find in hashMap
        while(curr) {
            if(strcmp(curr -> key, key) == 0)
                break;
            curr = curr -> hashNext;
        }
        // return if doesn't exist
        if(curr == NULL)
            return;
    
        // move curr to latest/tail of list
        if(curr -> prev == NULL) { // if head in list
            curr -> prev = list -> tail;
            list -> head = curr -> next;
            list -> head -> prev = NULL;
            list -> tail -> next = curr;
            list -> tail = curr;
            list -> tail -> next = NULL;
            return;
        }
    
        if(curr -> next == NULL) // already latest at tail
            return;
        
        // if curr in middle
        curr -> next -> prev = curr -> prev;
        curr -> prev -> next = curr -> next; 
        curr -> next = NULL;
        list -> tail -> next = curr;
        curr -> prev = list -> tail;
        list -> tail = curr;
    }

    addToList

    void addToList(LRUCache* cache, Node* valNode) {
        List* list = cache -> list;
        if(list -> size == list -> capacity)
            evictCache(cache);
        
        if (list -> head == NULL) {
            list -> head = list -> tail = valNode;
            list -> size = 1;   
            return; 
        }
        
        valNode -> prev = list -> tail;
        list -> tail -> next = valNode;
        list -> tail = valNode;
        list -> size = list -> size + 1;
        return;
    }

    We see that we evict the cache if the size of list reaches the capacity. Lets write that out too.

    void evictCache(LRUCache* cache) {
        Table* table = cache -> table;
        List* list = cache -> list;
    
        Node* entry = list -> head;
        // return if empty
        if(list -> head == NULL)
            return;
        // remove head and tail if only one node
        if(list -> head == list -> tail) {
            list -> head = NULL;
            list -> tail = NULL;
        } else  {
            // remove entry from list with multiple nodes
            list -> head = entry -> next;
            list -> size = list -> size - 1;
            list -> head -> prev = NULL;
        }
            
        // remove from map
        size_t hashCode = getHashCode(table, entry -> key);
        Node** indirect = &table->array[hashCode]; 
        while((*indirect) != entry)
            indirect = &(*indirect)->next;
        *indirect = entry -> next;
    
        free(entry);
    }

    Another function we keep using is the getHashCode which takes a char buffer and returns a index in the table. We will use a simple implementation of a hashing function for this.

    static size_t getHashCode(Table* table, const char* source) {    
        if (source == NULL)
            return 0;
        size_t hash = 0;
        while (*source != '\0') {
            char c = *source++;
            int a = c - '0';
            hash = (hash * 10) + a;     
        } 
        return hash % table -> capacity;
    }

    Now that we are able to add entries to the cache, lets write the function to retrieve entries.

    char* get(LRUCache* cache, char* key) {
        Table* table = cache -> table;
        size_t hashCode = getHashCode(table, key);
        Node* curr =  table -> array[hashCode];
        while(curr) {
            if(strcmp(curr -> key, key) == 0) {
                moveToFront(cache, key);
                return curr -> value;
            }
            curr = curr -> hashNext;
        }
        return NULL;
    }

    And thats it. Lets test this in the main function.

    int main() {
        int cacheSize = 3;
        LRUCache* cache = createCache(cacheSize);
    
        put(cache, "a" , "b");
        put(cache, "c" , "d");
        put(cache, "e" , "f");
        put(cache, "g" , "h");
        put(cache, "c" , "z");
        put(cache, "e" , "y");
    
        get(cache, "c");
    
        
        // print the cache contents from lru to mru
        Node* temp = cache -> list -> head;
        for(size_t i = 0; i < cache -> list -> size; i++) {
            printf("%s %s \n", temp -> key, temp -> value);
            temp = temp -> next;
        }
        return 1;
    }

    And this is the output.

    g h 
    e y 
    c z 

    You can find the entire source code at github.

    https://www.geeksforgeeks.org/lru-cache-implementation/

    // A C program to show implementation of LRU cache
    #include <stdio.h>
    #include <stdlib.h>
      
    // A Queue Node (Queue is implemented using Doubly Linked List)
    typedef struct QNode {
        struct QNode *prev, *next;
        unsigned pageNumber; // the page number stored in this QNode
    } QNode;
      
    // A Queue (A FIFO collection of Queue Nodes)
    typedef struct Queue {
        unsigned count; // Number of filled frames
        unsigned numberOfFrames; // total number of frames
        QNode *front, *rear;
    } Queue;
      
    // A hash (Collection of pointers to Queue Nodes)
    typedef struct Hash {
        int capacity; // how many pages can be there
        QNode** array; // an array of queue nodes
    } Hash;
      
    // A utility function to create a new Queue Node. The queue Node
    // will store the given 'pageNumber'
    QNode* newQNode(unsigned pageNumber)
    {
        // Allocate memory and assign 'pageNumber'
        QNode* temp = (QNode*)malloc(sizeof(QNode));
        temp->pageNumber = pageNumber;
      
        // Initialize prev and next as NULL
        temp->prev = temp->next = NULL;
      
        return temp;
    }
      
    // A utility function to create an empty Queue.
    // The queue can have at most 'numberOfFrames' nodes
    Queue* createQueue(int numberOfFrames)
    {
        Queue* queue = (Queue*)malloc(sizeof(Queue));
      
        // The queue is empty
        queue->count = 0;
        queue->front = queue->rear = NULL;
      
        // Number of frames that can be stored in memory
        queue->numberOfFrames = numberOfFrames;
      
        return queue;
    }
      
    // A utility function to create an empty Hash of given capacity
    Hash* createHash(int capacity)
    {
        // Allocate memory for hash
        Hash* hash = (Hash*)malloc(sizeof(Hash));
        hash->capacity = capacity;
      
        // Create an array of pointers for referring queue nodes
        hash->array = (QNode**)malloc(hash->capacity * sizeof(QNode*));
      
        // Initialize all hash entries as empty
        int i;
        for (i = 0; i < hash->capacity; ++i)
            hash->array[i] = NULL;
      
        return hash;
    }
      
    // A function to check if there is slot available in memory
    int AreAllFramesFull(Queue* queue)
    {
        return queue->count == queue->numberOfFrames;
    }
      
    // A utility function to check if queue is empty
    int isQueueEmpty(Queue* queue)
    {
        return queue->rear == NULL;
    }
      
    // A utility function to delete a frame from queue
    void deQueue(Queue* queue)
    {
        if (isQueueEmpty(queue))
            return;
      
        // If this is the only node in list, then change front
        if (queue->front == queue->rear)
            queue->front = NULL;
      
        // Change rear and remove the previous rear
        QNode* temp = queue->rear;
        queue->rear = queue->rear->prev;
      
        if (queue->rear)
            queue->rear->next = NULL;
      
        free(temp);
      
        // decrement the number of full frames by 1
        queue->count--;
    }
      
    // A function to add a page with given 'pageNumber' to both queue
    // and hash
    void Enqueue(Queue* queue, Hash* hash, unsigned pageNumber)
    {
        // If all frames are full, remove the page at the rear
        if (AreAllFramesFull(queue)) {
            // remove page from hash
            hash->array[queue->rear->pageNumber] = NULL;
            deQueue(queue);
        }
      
        // Create a new node with given page number,
        // And add the new node to the front of queue
        QNode* temp = newQNode(pageNumber);
        temp->next = queue->front;
      
        // If queue is empty, change both front and rear pointers
        if (isQueueEmpty(queue))
            queue->rear = queue->front = temp;
        else // Else change the front
        {
            queue->front->prev = temp;
            queue->front = temp;
        }
      
        // Add page entry to hash also
        hash->array[pageNumber] = temp;
      
        // increment number of full frames
        queue->count++;
    }
      
    // This function is called when a page with given 'pageNumber' is referenced
    // from cache (or memory). There are two cases:
    // 1. Frame is not there in memory, we bring it in memory and add to the front
    // of queue
    // 2. Frame is there in memory, we move the frame to front of queue
    void ReferencePage(Queue* queue, Hash* hash, unsigned pageNumber)
    {
        QNode* reqPage = hash->array[pageNumber];
      
        // the page is not in cache, bring it
        if (reqPage == NULL)
            Enqueue(queue, hash, pageNumber);
      
        // page is there and not at front, change pointer
        else if (reqPage != queue->front) {
            // Unlink rquested page from its current location
            // in queue.
            reqPage->prev->next = reqPage->next;
            if (reqPage->next)
                reqPage->next->prev = reqPage->prev;
      
            // If the requested page is rear, then change rear
            // as this node will be moved to front
            if (reqPage == queue->rear) {
                queue->rear = reqPage->prev;
                queue->rear->next = NULL;
            }
      
            // Put the requested page before current front
            reqPage->next = queue->front;
            reqPage->prev = NULL;
      
            // Change prev of current front
            reqPage->next->prev = reqPage;
      
            // Change front to the requested page
            queue->front = reqPage;
        }
    }
      
    // Driver program to test above functions
    int main()
    {
        // Let cache can hold 4 pages
        Queue* q = createQueue(4);
      
        // Let 10 different pages can be requested (pages to be
        // referenced are numbered from 0 to 9
        Hash* hash = createHash(10);
      
        // Let us refer pages 1, 2, 3, 1, 4, 5
        ReferencePage(q, hash, 1);
        ReferencePage(q, hash, 2);
        ReferencePage(q, hash, 3);
        ReferencePage(q, hash, 1);
        ReferencePage(q, hash, 4);
        ReferencePage(q, hash, 5);
      
        // Let us print cache frames after the above referenced pages
        printf("%d ", q->front->pageNumber);
        printf("%d ", q->front->next->pageNumber);
        printf("%d ", q->front->next->next->pageNumber);
        printf("%d ", q->front->next->next->next->pageNumber);
      
        return 0;
    }
    // A C program to show implementation of LRU cache
    #include <stdio.h>
    #include <stdlib.h>
      
    // A Queue Node (Queue is implemented using Doubly Linked List)
    typedef struct QNode {
        struct QNode *prev, *next;
        unsigned pageNumber; // the page number stored in this QNode
    } QNode;
      
    // A Queue (A FIFO collection of Queue Nodes)
    typedef struct Queue {
        unsigned count; // Number of filled frames
        unsigned numberOfFrames; // total number of frames
        QNode *front, *rear;
    } Queue;
      
    // A hash (Collection of pointers to Queue Nodes)
    typedef struct Hash {
        int capacity; // how many pages can be there
        QNode** array; // an array of queue nodes
    } Hash;
      
    // A utility function to create a new Queue Node. The queue Node
    // will store the given 'pageNumber'
    QNode* newQNode(unsigned pageNumber)
    {
        // Allocate memory and assign 'pageNumber'
        QNode* temp = (QNode*)malloc(sizeof(QNode));
        temp->pageNumber = pageNumber;
      
        // Initialize prev and next as NULL
        temp->prev = temp->next = NULL;
      
        return temp;
    }
      
    // A utility function to create an empty Queue.
    // The queue can have at most 'numberOfFrames' nodes
    Queue* createQueue(int numberOfFrames)
    {
        Queue* queue = (Queue*)malloc(sizeof(Queue));
      
        // The queue is empty
        queue->count = 0;
        queue->front = queue->rear = NULL;
      
        // Number of frames that can be stored in memory
        queue->numberOfFrames = numberOfFrames;
      
        return queue;
    }
      
    // A utility function to create an empty Hash of given capacity
    Hash* createHash(int capacity)
    {
        // Allocate memory for hash
        Hash* hash = (Hash*)malloc(sizeof(Hash));
        hash->capacity = capacity;
      
        // Create an array of pointers for referring queue nodes
        hash->array = (QNode**)malloc(hash->capacity * sizeof(QNode*));
      
        // Initialize all hash entries as empty
        int i;
        for (i = 0; i < hash->capacity; ++i)
            hash->array[i] = NULL;
      
        return hash;
    }
      
    // A function to check if there is slot available in memory
    int AreAllFramesFull(Queue* queue)
    {
        return queue->count == queue->numberOfFrames;
    }
      
    // A utility function to check if queue is empty
    int isQueueEmpty(Queue* queue)
    {
        return queue->rear == NULL;
    }
      
    // A utility function to delete a frame from queue
    void deQueue(Queue* queue)
    {
        if (isQueueEmpty(queue))
            return;
      
        // If this is the only node in list, then change front
        if (queue->front == queue->rear)
            queue->front = NULL;
      
        // Change rear and remove the previous rear
        QNode* temp = queue->rear;
        queue->rear = queue->rear->prev;
      
        if (queue->rear)
            queue->rear->next = NULL;
      
        free(temp);
      
        // decrement the number of full frames by 1
        queue->count--;
    }
      
    // A function to add a page with given 'pageNumber' to both queue
    // and hash
    void Enqueue(Queue* queue, Hash* hash, unsigned pageNumber)
    {
        // If all frames are full, remove the page at the rear
        if (AreAllFramesFull(queue)) {
            // remove page from hash
            hash->array[queue->rear->pageNumber] = NULL;
            deQueue(queue);
        }
      
        // Create a new node with given page number,
        // And add the new node to the front of queue
        QNode* temp = newQNode(pageNumber);
        temp->next = queue->front;
      
        // If queue is empty, change both front and rear pointers
        if (isQueueEmpty(queue))
            queue->rear = queue->front = temp;
        else // Else change the front
        {
            queue->front->prev = temp;
            queue->front = temp;
        }
      
        // Add page entry to hash also
        hash->array[pageNumber] = temp;
      
        // increment number of full frames
        queue->count++;
    }
      
    // This function is called when a page with given 'pageNumber' is referenced
    // from cache (or memory). There are two cases:
    // 1. Frame is not there in memory, we bring it in memory and add to the front
    // of queue
    // 2. Frame is there in memory, we move the frame to front of queue
    void ReferencePage(Queue* queue, Hash* hash, unsigned pageNumber)
    {
        QNode* reqPage = hash->array[pageNumber];
      
        // the page is not in cache, bring it
        if (reqPage == NULL)
            Enqueue(queue, hash, pageNumber);
      
        // page is there and not at front, change pointer
        else if (reqPage != queue->front) {
            // Unlink rquested page from its current location
            // in queue.
            reqPage->prev->next = reqPage->next;
            if (reqPage->next)
                reqPage->next->prev = reqPage->prev;
      
            // If the requested page is rear, then change rear
            // as this node will be moved to front
            if (reqPage == queue->rear) {
                queue->rear = reqPage->prev;
                queue->rear->next = NULL;
            }
      
            // Put the requested page before current front
            reqPage->next = queue->front;
            reqPage->prev = NULL;
      
            // Change prev of current front
            reqPage->next->prev = reqPage;
      
            // Change front to the requested page
            queue->front = reqPage;
        }
    }
      
    // Driver program to test above functions
    int main()
    {
        // Let cache can hold 4 pages
        Queue* q = createQueue(4);
      
        // Let 10 different pages can be requested (pages to be
        // referenced are numbered from 0 to 9
        Hash* hash = createHash(10);
      
        // Let us refer pages 1, 2, 3, 1, 4, 5
        ReferencePage(q, hash, 1);
        ReferencePage(q, hash, 2);
        ReferencePage(q, hash, 3);
        ReferencePage(q, hash, 1);
        ReferencePage(q, hash, 4);
        ReferencePage(q, hash, 5);
      
        // Let us print cache frames after the above referenced pages
        printf("%d ", q->front->pageNumber);
        printf("%d ", q->front->next->pageNumber);
        printf("%d ", q->front->next->next->pageNumber);
        printf("%d ", q->front->next->next->next->pageNumber);
      
        return 0;
    }
  • 相关阅读:
    WM_CHAR消息分析
    数据库OleDbConnection对象参考
    数据库使用Command对象进行数据库查询
    如何在VBNET中使用调试输出类Debug和Trace
    数据库与数据库连接
    数据库ADONETOleDbDataReader对象参考
    VBNET运行时处理对象事件(AddHandler和RemoveHandler)
    防火墙分类简述(班门弄斧了)
    杀毒防护类软件的组合转帖
    数据库ADONETOleDbCommand对象参考
  • 原文地址:https://www.cnblogs.com/sinferwu/p/16292520.html
Copyright © 2020-2023  润新知