性能优化学习笔记(1)-缓存系统

在现实的开发场景中,仅仅开发"正确运行的程序"往往不足以满足用户的需求,你的程序可能会在客户所能提供的硬件上运行的很缓慢;你可能只有一个微处理器,却需要满足巨大的性能开销目标;你可能需要在一个性能有限的设备上需要运行一些高级的渲染效果;这时候,你需要优化你的程序,针对一个已有系统的性能优化,根据我们"动手"的不同方向一般有如下几种情况:

**1.算法层面的优化:**简单到我们需要一个排序算法,不同算法的时间复杂度是不同的,甚至于面对不同的需求,选择不同的算法会得到不同的时间复杂度。

**2.代码使用层面的优化:**比如在函数中使用静态变量,由于在同一函数中动态变量只会初始化一次,我们可以对一些需要读取和加载的方式使用静态变量的方式,既可以满足RAII的方式,在使用的时候再加载,也可以实现只加载一次,避免重复加载。

**3.调用热点层面的优化:**在一些场景中,我们会得到一些性能热点,比如某些语句会被多次调用,某些函数会被放在循环里进行调用,这时优化这些热点语句可以起到事半功倍的效果。一般在一个系统中,这些代码热点往往包括IO,创建分配内存等。

面对不同的情况,我们需要不同的手段,这些都是我们手中一些工具:比如并行思想,把一些可以同时进行的工作拆分成不同的线程,用多线程的方式,比如我们需要读取两套不同的配置,而这两套配置互不相关,这时候可以使用多线程的方式,将两套配置的读取拆分到两个线程上,多线程方法和思维在性能优化中是一个很重要的手段,本套学习笔记后续会有很多篇幅介绍C++的并发编程。另外就是本文介绍的缓存思想。

缓存思想最早来源于硬件,最早的思想萌芽可以追溯到上世纪60年代,来自于CPU计算速度远高于内存读写速度,这样应用热点优化的思想,加入一块缓存区域将常用的数据暂时写到上卖弄,硬件上有了"从动存储器"的概念,如今已经发展出"分布式缓存"和"内容分发网络"等概念。

在软件开发层面,缓存思维是对于一些数据,我们可以采取"空间换时间"的思想,把一些需要IO加载的数据提前批量读到内存当中,当使用时,优先从内存中查找,找不到的情况下再去用IO的方式读取。

使用缓存思想,可以减少数据访问路径,从而达到提升性能的效果。比如一个需要加载字体信息的系统使用fontconfig库来读取,读取字体库的代码逻辑需要占用IO,是代码优化的热点,而单次的加载开销是不可避免的,但是我们可以通过缓存来避免重复加载,对于多次使用的字体,可以一直保存在内存之中,如图所示。

对于读取字体信息的请求,首先在缓存数据结构中获得对应信息,如果存在那么直接返回,如果没有读到,那么继续调用fontconfig的函数来获得字体信息,把字体信息存储到缓存系统中,同时返回这个字体信息。

缓存这个思想,本质上是"时间换空间",然而空间并不能被无限制的使用,因此我们需要更好的缓存策略来有限制的使用内存。所以缓存策略的核心目标,就是利用有限的内存来提升系统的访问效率,这时候就要设计好缓存的淘汰机制,即当内存放不下的时候,需要保留哪些数据,淘汰哪些数据,有一些主流的思路可以参考:

|--------|----------------------------|---------------------|---------------|----------|----------------------|
| 简称 | 描述 | 数据结构 | 时间复杂度 | 空间复杂度 | 适用情况 |
| FIFO | 先进先出,顾名思义,先进入缓存的优先被淘汰 | 队列 | O(1) | 没有额外开销 | 顺序访问,没有数据访问频率优先级 |
| TTL | 有效时间缓存,一旦超过设定的时间,缓存项过期 | 哈希表 时间堆 | O(logn) | 多了对应的哈希表 | 有时效性的访问,比如登录等 |
| Random | 随机选择淘汰数据 | 列表 | O(1) | 没有额外开销 | 没有频率差异的低成本容忍场景 |
| LRU | 淘汰最久未被访问的数据,即链表最末端的数据被最早淘汰 | 哈希表 双向链表 | O(1) | 多了对应的哈希表 | 网络缓存 |
| LFU | 淘汰访问频率最低的数据,淘汰频率最低的项目 | 哈希表(键值对) 哈希表(频率键值对) | O(1)- O(logn) | 多了频率键值对 | 高频访问数据场景(比如微博热搜) |
| ARC | 自适应替换缓存 | 两个双向列表 两个哈希表 | O(1)- O(logn) | 多了一组额外数据 | 结合LRU和LFU优点,缺点是额外空间多 |

其中ARC结合了LRU和LFU优势,在高负载或者不同访问模式下提升内存中缓存项的命中率。通过动态自适应不同场景,比传统的LRU或LFU算法更有效。

ARC的核心就是两套列链表和哈希表数据结构,第一套是LRU结构,首先添加到缓存中的新数据先进入这个链表,第二次访问后进入第二套数据结构,当数据数量超过预定的容量后,选择淘汰机制,通过检查LRU和LFU列表中的数量,来判断这个缓存使用的是数据集较大还是访问频率比较高,如果是数据集较大,则按LRU的规则删除数据最末尾的项目,反之则按照LFU删除访问频率最低的项目,其核心代码为:

cpp 复制代码
void ARCCache::put(int key, const Data &value) {
    Data ignored;
    // 更新数据
    if (get(key, ignored)) {
        mapT2[key].value = value;
        return;
    }
    
    // 是否已经在LRU列表中
    if (mapB1.find(key) != mapB1.end()) {
        std::size_t b1 = mapB1.size();
        std::size_t b2 = mapB2.size();
        std::size_t delta = 1;
        if (b1 == 0) delta = (b2 == 0 ? 1 : b2);
        else delta = std::max<std::size_t>(b2 / b1, 1);
        p = std::min(capacity, p + delta);
        replace(key);

        B1.erase(mapB1[key]);
        mapB1.erase(key);

        T2.push_front(key);
        mapT2[key] = {value, T2.begin()};
        return;
    }
    // 是否已经在LFU列表中
    if (mapB2.find(key) != mapB2.end()) {
        std::size_t b1 = mapB1.size();
        std::size_t b2 = mapB2.size();
        std::size_t delta = 1;
        if (b2 == 0) delta = (b1 == 0 ? 1 : b1);
        else delta = std::max<std::size_t>(b1 / b2, 1);
        p = (p >= delta) ? p - delta : 0;
        replace(key);

        B2.erase(mapB2[key]);
        mapB2.erase(key);

        T2.push_front(key);
        mapT2[key] = {value, T2.begin()};
        return;
    }

    // 淘汰机制
    if (T1.size() + B1.size() == capacity) {
        if (T1.size() < capacity) {
            int old = B1.back();
            B1.pop_back();
            mapB1.erase(old);
            replace(key);
        } else {
            int old = T1.back();
            T1.pop_back();
            mapT1.erase(old);
        }
    } else {
        std::size_t total = T1.size() + T2.size() + B1.size() + B2.size();
        if (total >= capacity) {
            if (total == 2 * capacity) {
                int old = B2.back();
                B2.pop_back();
                mapB2.erase(old);
            }
            replace(key);
        }
    }

    T1.push_front(key);
    mapT1[key] = {value, T1.begin()};
}

这段缓存机制已经被集成到我的Crisp项目中,后续打算参照开源项目将更多缓存策略集成进来,可供开发者缓存使用。

相关推荐
三水不滴2 小时前
消息队列消费性能优化:批量消费 + 手动 ACK 提升吞吐量
经验分享·笔记·中间件·性能优化
山岚的运维笔记2 小时前
SQL Server笔记 -- 第80章:分页
java·数据库·笔记·sql·microsoft·sqlserver
马猴烧酒.2 小时前
【JAVA算法|hot100】哈希类型题目详解笔记
java·笔记
Blue16°2 小时前
Day33:英语翻译 + 单词打卡
笔记
JELEE.2 小时前
原生微信小程序开发笔记
笔记·微信小程序
FakeOccupational2 小时前
【电路笔记 STM32】STM32CubeMX配置&自动移植FreeRTOS + STM32&FreeRTOS点灯的最简单示例
笔记·stm32·单片机
马猴烧酒.2 小时前
【JAVA算法|hot100】数组类型题目详解笔记
java·笔记
浅念-2 小时前
C++ STL list 容器
开发语言·数据结构·c++·经验分享·笔记·算法·list
眼镜哥(with glasses)3 小时前
0215笔记-面向开发者的LLM入门课程-课时10:文本扩展-课题11:聊天机器人
笔记