• 首页
  • 粮食
  • 蔬菜
  • 果品
  • 水产
  • 酒水
  • 饮料
  • 茶叶
  • 畜禽
  • 食用油
  • 资讯
logo
  • 首页>
  • 水产 >
  • 正文

焦点关注:LRU缓存替换策略及C#实现

2023-04-05 18:20:14 来源:博客园
目录LRU缓存替换策略核心思想不适用场景算法基本实现算法优化进一步优化BenchmarkLRU缓存替换策略

缓存是一种非常常见的设计,通过将数据缓存到访问速度更快的存储设备中,来提高数据的访问速度,如内存、CPU缓存、硬盘缓存等。

但与缓存的高速相对的是,缓存的成本较高,因此容量往往是有限的,当缓存满了之后,就需要一种策略来决定将哪些数据移除出缓存,以腾出空间来存储新的数据。


(资料图片)

这样的策略被称为缓存替换策略(Cache Replacement Policy)。

常见的缓存替换策略有:FIFO(First In First Out)、LRU(Least Recently Used)、LFU(Least Frequently Used)等。

今天给大家介绍的是LRU算法。

核心思想

LRU算法基于这样一个假设:如果数据最近被访问过,那么将来被访问的几率也更高。

大部分情况下这个假设是成立的,因此LRU算法也是比较常用的缓存替换策略。

基于这个假设,我们在实现的时候,需要维护一个有序的数据结构,来记录数据的访问历史,当缓存满了之后,就可以根据这个数据结构来决定将哪些数据移除出缓存。

不适用场景

但如果数据的访问模式不符合LRU算法的假设,那么LRU算法就会失效。

例如:数据的访问模式是周期性的,那么LRU算法就会把周期性的数据淘汰掉,这样就会导致缓存命中率的下降。

换个说法比如,如果现在缓存的数据只在白天被访问,晚上访问的是另一批数据,那么在晚上,LRU算法就会把白天访问的数据淘汰掉,第二天白天又会把昨天晚上访问的数据淘汰掉,这样就会导致缓存命中率的下降。

后面有时间会给大家介绍LFU(Least Frequently Used)算法,以及LFU和LRU的结合LFRU(Least Frequently and Recently Used)算法,可以有效的解决这个问题。

算法基本实现

上文提到,LRU算法需要维护一个有序的数据结构,来记录数据的访问历史。通常我们会用双向链表来实现这个数据结构,因为双向链表可以在O(1)的时间复杂度内往链表的头部或者尾部插入数据,以及在O(1)的时间复杂度内删除数据。

我们将数据存储在双向链表中,每次访问数据的时候,就将数据移动到链表的尾部,这样就可以保证链表的尾部就是最近访问的数据,链表的头部就是最久没有被访问的数据。

当缓存满了之后,如果需要插入新的数据,因为链表的头部就是最久没有被访问的数据,所以我们就可以直接将链表的头部删除,然后将新的数据插入到链表的尾部。

如果我们要实现一个键值对的缓存,我们可以用一个哈希表来存储键值对,这样就可以在O(1)的时间复杂度内完成查找操作,.NET 中我们可以使用 Dictionary。

同时我们使用 LinkedList 来作为双向链表的实现,存储缓存的 key,以此记录数据的访问历史。

我们在每次操作 Dictionary 进行插入、删除、查找的时候,都需要将对应的 key 也插入、删除、移动到链表的尾部。

// 实现 IEnumerable 接口,方便遍历public class LRUCache : IEnumerable>{    private readonly LinkedList _list;    private readonly Dictionary _dictionary;    private readonly int _capacity;        public LRUCache(int capacity)    {        _capacity = capacity;        _list = new LinkedList();        _dictionary = new Dictionary();    }    public TValue Get(TKey key)    {        if (_dictionary.TryGetValue(key, out var value))        {            // 在链表中删除 key,然后将 key 添加到链表的尾部            // 这样就可以保证链表的尾部就是最近访问的数据,链表的头部就是最久没有被访问的数据            // 但是在链表中删除 key 的时间复杂度是 O(n),所以这个算法的时间复杂度是 O(n)            _list.Remove(key);            _list.AddLast(key);            return value;        }        return default;    }    public void Put(TKey key, TValue value)    {        if (_dictionary.TryGetValue(key, out _))        {            // 如果插入的 key 已经存在,将 key 对应的值更新,然后将 key 移动到链表的尾部            _dictionary[key] = value;            _list.Remove(key);            _list.AddLast(key);        }        else        {                      if (_list.Count == _capacity)            {                // 缓存满了,删除链表的头部,也就是最久没有被访问的数据                _dictionary.Remove(_list.First.Value);                _list.RemoveFirst();            }            _list.AddLast(key);            _dictionary.Add(key, value);        }    }    public void Remove(TKey key)    {        if (_dictionary.TryGetValue(key, out _))        {            _dictionary.Remove(key);            _list.Remove(key);        }    }    public IEnumerator> GetEnumerator()    {        foreach (var key in _list)        {            yield return new KeyValuePair(key, _dictionary[key]);        }    }    IEnumerator IEnumerable.GetEnumerator()    {        return GetEnumerator();    }}
var lruCache = new LRUCache(4);lruCache.Put(1, 1);lruCache.Put(2, 2);lruCache.Put(3, 3);lruCache.Put(4, 4);Console.WriteLine(string.Join(" ", lruCache));Console.WriteLine(lruCache.Get(2));Console.WriteLine(string.Join(" ", lruCache));lruCache.Put(5, 5);Console.WriteLine(string.Join(" ", lruCache));lruCache.Remove(3);Console.WriteLine(string.Join(" ", lruCache));

输出:

[1, 1] [2, 2] [3, 3] [4, 4] // 初始化2                           // 访问 2[1, 1] [3, 3] [4, 4] [2, 2] // 2 移动到链表尾部[3, 3] [4, 4] [2, 2] [5, 5] // 插入 5[4, 4] [2, 2] [5, 5]        // 删除 3
算法优化

上面的实现中,对缓存的查询、插入、删除都会涉及到链表中数据的删除(移动也是删除再插入)。

因为我们在 LinkedList 中存储的是 key,所以我们需要先通过 key 在链表中找到对应的节点,然后再进行删除操作,这就导致了链表的删除操作的时间复杂度是 O(n)。

虽然 Dictionary 的查找、插入、删除操作的时间复杂度都是 O(1),但因为链表操作的时间复杂度是 O(n),整个算法的最差时间复杂度是 O(n)。

算法优化的关键在于如何降低链表的删除操作的时间复杂度。

优化思路:

在 Dictionary 中存储 key 和 LinkedList 中节点的映射关系在 LinkedList 的节点中存储 key-value

也就是说,我们让两个本来不相关的数据结构之间产生联系。

不管是在插入、删除、查找缓存的时候,都可以通过这种联系来将时间复杂度降低到 O(1)。

通过 key 在 Dictionary 中找到对应的节点,然后再从 LinkedList 节点中取出 value,时间复杂度是 O(1)LinkedList 删除数据之前,先通过 key 在 Dictionary 中找到对应的节点,然后再删除,这样就可以将链表的删除操作的时间复杂度降低到 O(1)LinkedList 删除头部节点时,因为节点中存储了 key,所以我们可以通过 key 在 Dictionary 中删除对应的节点,时间复杂度是 O(1)
public class LRUCache_V2 : IEnumerable>{    private readonly LinkedList> _list;        private readonly Dictionary>> _dictionary;        private readonly int _capacity;        public LRUCache_V2(int capacity)    {        _capacity = capacity;        _list = new LinkedList>();        _dictionary = new Dictionary>>();    }        public TValue Get(TKey key)    {        if (_dictionary.TryGetValue(key, out var node))        {            _list.Remove(node);            _list.AddLast(node);            return node.Value.Value;        }                return default;    }        public void Put(TKey key, TValue value)    {        if (_dictionary.TryGetValue(key, out var node))        {            node.Value = new KeyValuePair(key, value);            _list.Remove(node);            _list.AddLast(node);        }        else        {            if (_list.Count == _capacity)            {                _dictionary.Remove(_list.First.Value.Key);                _list.RemoveFirst();            }                        var newNode = new LinkedListNode>(new KeyValuePair(key, value));            _list.AddLast(newNode);            _dictionary.Add(key, newNode);        }    }        public void Remove(TKey key)    {        if (_dictionary.TryGetValue(key, out var node))        {            _dictionary.Remove(key);            _list.Remove(node);        }    }    public IEnumerator> GetEnumerator()    {        return _list.GetEnumerator();    }    IEnumerator IEnumerable.GetEnumerator()    {        return GetEnumerator();    }}
进一步优化

因为我们对 双向链表 的存储需求是定制化的,要求节点中存储 key-value,直接使用 C# 的 LinkedList 我们就需要用 KeyValuePair 这样的结构来间接存储,会导致一些不必要的内存开销。

我们可以自己实现一个双向链表,这样就可以直接在节点中存储 key-value,从而减少内存开销。

public class LRUCache_V3{    private readonly DoubleLinkedListNode _head;    private readonly DoubleLinkedListNode _tail;    private readonly Dictionary> _dictionary;    private readonly int _capacity;    public LRUCache_V3(int capacity)    {        _capacity = capacity;        _head = new DoubleLinkedListNode();        _tail = new DoubleLinkedListNode();        _head.Next = _tail;        _tail.Previous = _head;        _dictionary = new Dictionary>();    }    public TValue Get(TKey key)    {        if (_dictionary.TryGetValue(key, out var node))        {            RemoveNode(node);            AddLastNode(node);            return node.Value;        }        return default;    }    public void Put(TKey key, TValue value)    {        if (_dictionary.TryGetValue(key, out var node))        {            RemoveNode(node);            AddLastNode(node);            node.Value = value;        }        else        {            if (_dictionary.Count == _capacity)            {                var firstNode = RemoveFirstNode();                _dictionary.Remove(firstNode.Key);            }            var newNode = new DoubleLinkedListNode(key, value);            AddLastNode(newNode);            _dictionary.Add(key, newNode);        }    }    public void Remove(TKey key)    {        if (_dictionary.TryGetValue(key, out var node))        {            _dictionary.Remove(key);            RemoveNode(node);        }    }    private void AddLastNode(DoubleLinkedListNode node)    {        node.Previous = _tail.Previous;        node.Next = _tail;        _tail.Previous.Next = node;        _tail.Previous = node;    }    private DoubleLinkedListNode RemoveFirstNode()    {        var firstNode = _head.Next;        _head.Next = firstNode.Next;        firstNode.Next.Previous = _head;        firstNode.Next = null;        firstNode.Previous = null;        return firstNode;    }    private void RemoveNode(DoubleLinkedListNode node)    {        node.Previous.Next = node.Next;        node.Next.Previous = node.Previous;        node.Next = null;        node.Previous = null;    }        internal class DoubleLinkedListNode    {            public DoubleLinkedListNode()        {        }        public DoubleLinkedListNode(TKey key, TValue value)        {            Key = key;            Value = value;        }        public TKey Key { get; set; }                public TValue Value { get; set; }        public DoubleLinkedListNode Previous { get; set; }        public DoubleLinkedListNode Next { get; set; }    }}
Benchmark

使用 BenchmarkDotNet 对3个版本进行性能测试对比。

[MemoryDiagnoser]public class WriteBenchmarks{    // 保证写入的数据有一定的重复性,借此来测试LRU的最差时间复杂度    private const int Capacity = 1000;    private const int DataSize = 10_0000;        private List _data;    [GlobalSetup]    public void Setup()    {        _data = new List();        var shared = Random.Shared;        for (int i = 0; i < DataSize; i++)        {            _data.Add(shared.Next(0, DataSize / 10));        }    }        [Benchmark]    public void LRUCache_V1()    {        var cache = new LRUCache(Capacity);        foreach (var item in _data)        {            cache.Put(item, item);        }    }        [Benchmark]    public void LRUCache_V2()    {        var cache = new LRUCache_V2(Capacity);        foreach (var item in _data)        {            cache.Put(item, item);        }    }        [Benchmark]    public void LRUCache_V3()    {        var cache = new LRUCache_V3(Capacity);        foreach (var item in _data)        {            cache.Put(item, item);        }    }}public class ReadBenchmarks{    // 保证写入的数据有一定的重复性,借此来测试LRU的最差时间复杂度    private const int Capacity = 1000;    private const int DataSize = 10_0000;        private List _data;    private LRUCache _cacheV1;    private LRUCache_V2 _cacheV2;    private LRUCache_V3 _cacheV3;    [GlobalSetup]    public void Setup()    {        _cacheV1 = new LRUCache(Capacity);        _cacheV2 = new LRUCache_V2(Capacity);        _cacheV3 = new LRUCache_V3(Capacity);        _data = new List();        var shared = Random.Shared;        for (int i = 0; i < DataSize; i++)        {            int dataToPut  = shared.Next(0, DataSize / 10);            int dataToGet = shared.Next(0, DataSize / 10);            _data.Add(dataToGet);            _cacheV1.Put(dataToPut, dataToPut);            _cacheV2.Put(dataToPut, dataToPut);            _cacheV3.Put(dataToPut, dataToPut);        }    }        [Benchmark]    public void LRUCache_V1()    {        foreach (var item in _data)        {            _cacheV1.Get(item);        }    }        [Benchmark]    public void LRUCache_V2()    {        foreach (var item in _data)        {            _cacheV2.Get(item);        }    }        [Benchmark]    public void LRUCache_V3()    {        foreach (var item in _data)        {            _cacheV3.Get(item);        }    }}

写入性能测试结果:

|      Method |      Mean |     Error |    StdDev |    Median |     Gen0 |     Gen1 | Allocated ||------------ |----------:|----------:|----------:|----------:|---------:|---------:|----------:|| LRUCache_V1 | 16.890 ms | 0.3344 ms | 0.8012 ms | 16.751 ms | 750.0000 | 218.7500 |   4.65 MB || LRUCache_V2 |  7.193 ms | 0.1395 ms | 0.3958 ms |  7.063 ms | 703.1250 | 226.5625 |   4.22 MB || LRUCache_V3 |  5.761 ms | 0.1102 ms | 0.1132 ms |  5.742 ms | 585.9375 | 187.5000 |   3.53 MB |

查询性能测试结果:

|      Method |      Mean |     Error |    StdDev |    Gen0 | Allocated ||------------ |----------:|----------:|----------:|--------:|----------:|| LRUCache_V1 | 19.475 ms | 0.3824 ms | 0.3390 ms | 62.5000 |  474462 B || LRUCache_V2 |  1.994 ms | 0.0273 ms | 0.0242 ms |       - |       4 B || LRUCache_V3 |  1.595 ms | 0.0187 ms | 0.0175 ms |       - |       3 B |

关键词:

    为您推荐

  • 焦点关注:LRU缓存替换策略及C#实现

    水产2023-04-05
  • 热点!卫星遥感、北斗定位、无人机巡航……“科技”赋能实现“数字”防火

    水产2023-04-05
  • 生命的终点,她用温情守护-每日速读

    水产2023-04-05
  • 那一刻我长大了400字_那一刻我长大了300字-今日热讯

    水产2023-04-05
  • 披萨怎么做(披萨)_环球速递

    水产2023-04-05
  • 中国经济快速恢复将提升地区和全球增长前景

    水产2023-04-05
  • 抢抓机遇发力先进制造业

    水产2023-04-05
  • 4月份全国自然灾害风险形势发布:环球热门

    水产2023-04-05
  • 世界热议:恒源煤电股票,600971(恒源煤电)这个股票怎么样?我9.66买的

    水产2023-04-05
  • 内蒙古森林消防总队驻皖队伍把稳“五战”风向标 高效助力撤防工作

    水产2023-04-05
  • 环球看热讯:山特TG500

    水产2023-04-04
  • 头条:越秀地产前三月销售438亿 同比上升217%

    水产2023-04-04
  • 拼多多宣布组织架构升级,赵佳臻出任联席CEO与陈磊搭档-世界播资讯

    水产2023-04-04
  • 艾力斯(688578):上海艾力斯医药科技股份有限公司自愿披露关于伏美替尼针对EGFR或HER2突变晚期NSCLC患者的IB期临床试验获得药物临床试验批准通知书-环球新要闻

    水产2023-04-04
  • 干货| 动态更新(热更新)机制及技术原理分享-世界观速讯

    水产2023-04-04
  • 雅艺科技(301113)4月4日主力资金净卖出109.77万元

    水产2023-04-04
  • 金自天正:人工神经元相关情况请参考公司往期|世界热消息

    水产2023-04-04
  • 广州城市理工学院学子在“大中华设计比赛”斩获季军-环球看热讯

    水产2023-04-04
  • 哥哥结婚需要送礼物或给份子钱吗|环球通讯

    水产2023-04-04
  • 清明祭扫请注意!这些物品不能带上火车

    水产2023-04-04

果品

  • 北京2022年冬奥会、冬残奥会奖牌“同心”正式发布
  • 冬奥故事会丨一图了解冬奥会历届奖牌
  • 同心筑梦向未来——写在北京冬奥会开幕倒计时100天之际
  • 外交部:美国针对亚裔仇恨犯罪数字令人痛心

蔬菜

  • 说好“一梯一户”却成了“两梯两户”,买方能否解除合同?
  • 更高水平开放合作助力中国东盟经贸发展迎新机遇
  • 9被告人犯侵犯著作权罪被判刑罚
  • 玉渊谭天丨中美再通话,“建设性”很重要
  • 环球时报社评:中美经贸需要建设性对话
  • 俄媒:莫斯科扩大新冠感染新疗法试点范围
  • 冰雪之约 中国之邀 | 追赶的勇气
  • 中国第20批赴黎维和建筑工兵分队完成“VA-2”道路排水系统修缮任务
  • 中国常驻联合国代表团举办恢复联合国合法席位50周年图片展
  • 美专家认为三大原因导致美国供应链危机

Copyright   2015-2022 北冰洋食品网 版权所有  备案号:沪ICP备2020036824号-3   联系邮箱:562 66 29@qq.com