一、LRU缓存

LRU缓存

  • 1.LRU缓存介绍
  • 2.LRU缓存实现
  • 3.LRU缓存总结
    • 3.1 LRU 缓存的应用
    • 3.2 LRU 缓存的优缺点

1.LRU缓存介绍

LRU是Least Recently Used 的缩写,意为“最近最少使用”。它是一种常见的缓存淘汰策略,用于在缓存容量有限时,决定哪些数据需要被删除以腾出空间。
LRU 缓存的基本原则是:
①优先保留最近被访问的数据,因为这些数据在近期被再次访问的概率更高。
②淘汰最近最少使用的数据,因为它们被再次访问的可能性较小。

2.LRU缓存实现

接下来我将通过c语言中的glib库来实现一个LRU缓存结构。
首先给出结构体定义:

struct lruCache {
    GList *elem_queue;  // 使用 GList 作为双向链表存储缓存元素。

    int max_size;       // 缓存最大容量,<0 表示无限大小(INFI_Cache)。
    int size;           // 当前缓存已使用的大小。

    double hit_count;   // 命中次数统计。
    double miss_count;  // 未命中次数统计。

    void (*free_elem)(void *);                  // 用户定义的释放元素函数。
    int (*hit_elem)(void* elem, void* user_data); // 判断命中元素的回调函数。
};

需要实现如下功能:

struct lruCache* new_lru_cache(int size, void (*free_elem)(void *),
		int (*hit_elem)(void* elem, void* user_data));
// 创建一个新的 LRU 缓存,指定容量和自定义的释放与命中回调函数。

void free_lru_cache(struct lruCache*);
// 释放缓存及其中的所有元素。

void* lru_cache_lookup(struct lruCache*, void* user_data);
// 查找元素,若命中,将其移到链表头部;未命中返回 NULL。

void* lru_cache_lookup_without_update(struct lruCache* c, void* user_data);
// 查找元素但不更新其在链表中的顺序。

void* lru_cache_hits(struct lruCache* c, void* user_data,
		int (*hit)(void* elem, void* user_data));
// 模拟命中某个元素,满足自定义命中条件后将元素移到链表头部。

void lru_cache_kicks(struct lruCache* c, void* user_data,
		int (*func)(void* elem, void* user_data));
// 删除满足用户自定义条件的元素。

void lru_cache_insert(struct lruCache *c, void* data,
		void (*victim)(void*, void*), void* user_data);
// 插入新数据到缓存,若缓存已满则淘汰最久未使用的元素,并调用 victim 函数处理被淘汰的数据。

int lru_cache_is_full(struct lruCache*);
// 检查缓存是否已满,已满返回 1,未满返回 0。

具体实现代码:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "lru_cache.h"

struct lruCache* new_lru_cache(int size, void (*free_elem)(void *),
		int (*hit_elem)(void* elem, void* user_data)) {
	struct lruCache* c = (struct lruCache*) malloc(sizeof(struct lruCache));

	c->elem_queue = NULL;

	c->max_size = size;
	c->size = 0;
	c->hit_count = 0;
	c->miss_count = 0;

	c->free_elem = free_elem;
	c->hit_elem = hit_elem;

	return c;
}

void free_lru_cache(struct lruCache *c) {
    if (c == NULL) return;  // 防止对 NULL 指针调用

    if (c->elem_queue != NULL) {
        // 确保对每个元素调用释放函数
        g_list_free_full(c->elem_queue, c->free_elem);
        c->elem_queue = NULL;  // 清空队列,防止重复释放
    }

    // 清理 lruCache 结构体本身
    free(c);
}

/* find a item in cache matching the condition */
void* lru_cache_lookup(struct lruCache* c, void* user_data) {
    // 获取链表的第一个节点(链表头部)
    GList* elem = g_list_first(c->elem_queue);

    // 遍历链表,查找匹配的元素
    while (elem) {
        /*
         * 使用回调函数 hit_elem 判断当前节点的数据是否与 user_data 匹配。
         * 回调函数由用户提供,自定义匹配逻辑。
         */
        if (c->hit_elem(elem->data, user_data))
            break;  // 找到匹配的元素,退出循环

        // 继续遍历下一个节点
        elem = g_list_next(elem);
    }

    // 如果找到匹配的元素
    if (elem) {
        /*
         * 将命中的元素移到链表头部,保持 LRU 缓存的访问顺序。
         * 1. 先从链表中移除该元素。
         * 2. 将该元素连接到链表头部。
         */
        c->elem_queue = g_list_remove_link(c->elem_queue, elem);
        c->elem_queue = g_list_concat(elem, c->elem_queue);

        // 增加缓存命中计数
        c->hit_count++;

        // 返回命中元素的数据
        return elem->data;
    } else {
        // 如果未找到匹配的元素,增加缓存未命中计数
        c->miss_count++;
        return NULL;  // 返回 NULL 表示未命中
    }
}

void* lru_cache_lookup_without_update(struct lruCache* c, void* user_data) {
	GList* elem = g_list_first(c->elem_queue);
	while (elem) {
		if (c->hit_elem(elem->data, user_data))
			break;
		elem = g_list_next(elem);
	}
	if (elem) {
		return elem->data;
	} else {
		return NULL;
	}
}

/*
 * Hit an existing elem for simulating an insertion of it.
 */
void* lru_cache_hits(struct lruCache* c, void* user_data,
		int (*hit)(void* elem, void* user_data)) {
	GList* elem = g_list_first(c->elem_queue);
	while (elem) {
		if (hit(elem->data, user_data))
			break;
		elem = g_list_next(elem);
	}
	if (elem) {
		c->elem_queue = g_list_remove_link(c->elem_queue, elem);
		c->elem_queue = g_list_concat(elem, c->elem_queue);
		return elem->data;
	} else {
		return NULL;
	}
}

/*
 * We know that the data does not exist!
 */
void lru_cache_insert(struct lruCache *c, void* data,
                      void (*func)(void*, void*), void* user_data) {
    void *victim = NULL; // 存储被淘汰的数据

    // 检查缓存是否已满
    if (c->max_size > 0 && c->size == c->max_size) {
        // 获取链表尾部的节点(最久未使用的数据)
        GList *last = g_list_last(c->elem_queue);

        // 从链表中移除尾部节点
        c->elem_queue = g_list_remove_link(c->elem_queue, last);

        // 保存被淘汰的数据
        victim = last->data;

        // 释放链表节点(但不释放节点内的数据)
        g_list_free_1(last);

        // 更新缓存大小
        c->size--;
    }

    // 将新数据插入到链表头部(表示最近使用)
    c->elem_queue = g_list_prepend(c->elem_queue, data);

    // 更新缓存大小
    c->size++;

    // 如果有被淘汰的数据
    if (victim) {
        // 调用用户自定义回调函数处理被淘汰的数据(如果提供了 func)
        if (func)
            func(victim, user_data);

        // 调用 free_elem 回调释放被淘汰的数据
        c->free_elem(victim);
    }
}

/*
 * 从缓存中移除符合用户定义条件的元素。
 * 
 * 参数:
 *   c          - 指向 lruCache 结构体的指针。
 *   user_data  - 用户自定义的数据,用于传递给回调函数 func。
 *   func       - 用户自定义的回调函数,用于判断当前元素是否需要被移除。
 *                返回非 0(true)表示移除该元素,返回 0(false)继续遍历。
 */
void lru_cache_kicks(struct lruCache* c, void* user_data,
                     int (*func)(void* elem, void* user_data)) {
    // 从链表尾部开始遍历(最久未使用的数据)
    GList* elem = g_list_last(c->elem_queue);

    // 遍历链表,向前移动,查找符合条件的节点
    while (elem) {
        /*
         * 调用用户提供的回调函数 func,判断当前节点的数据是否符合移除条件。
         * 参数:
         *   elem->data  - 当前节点存储的数据。
         *   user_data   - 用户提供的上下文数据。
         */
        if (func(elem->data, user_data)) 
            break; // 如果找到符合条件的节点,退出循环

        elem = g_list_previous(elem); // 移动到前一个节点
    }

    // 如果找到了符合条件的节点
    if (elem) {
        /*
         * 1. 从链表中移除该节点(但不释放节点内存和数据)。
         *    g_list_remove_link 返回移除后的链表。
         */
        c->elem_queue = g_list_remove_link(c->elem_queue, elem);

        /*
         * 2. 释放节点中存储的数据。
         *    调用用户提供的 free_elem 函数,确保数据被正确释放,防止内存泄漏。
         */
        c->free_elem(elem->data);

        /*
         * 3. 释放链表节点本身的内存。
         *    注意:g_list_free_1 只释放 GList 结构,不释放节点数据。
         */
        g_list_free_1(elem);

        // 4. 更新缓存大小
        c->size--;
    }
}


int lru_cache_is_full(struct lruCache* c) {
	return c->size >= c->max_size ? 1 : 0;
}

简单测试代码:

#include <stdio.h>
#include <stdlib.h>
#include "lru_cache.h"

// 自定义释放函数:释放节点数据
void free_data(void* data) {
    printf("Freeing data: %d\n", *(int*)data);
    free(data);
}

// 自定义匹配函数:判断数据是否匹配用户输入
int match_data(void* elem, void* user_data) {
    return (*(int*)elem == *(int*)user_data);
}

// 主函数:测试 LRU 缓存
int main() {
    printf("---- Testing LRU Cache ----\n");

    // 创建一个容量为 3 的 LRU 缓存
    struct lruCache* cache = new_lru_cache(3, free_data, match_data);

    // 插入测试数据
    int* a = malloc(sizeof(int)); *a = 1;
    int* b = malloc(sizeof(int)); *b = 2;
    int* c = malloc(sizeof(int)); *c = 3;
    int* d = malloc(sizeof(int)); *d = 4;

    printf("Inserting: 1, 2, 3\n");
    lru_cache_insert(cache, a, NULL, NULL);
    lru_cache_insert(cache, b, NULL, NULL);
    lru_cache_insert(cache, c, NULL, NULL);

    // 查找数据:命中情况
    int target = 2;
    int* result = lru_cache_lookup(cache, &target);
    if (result) {
        printf("Cache hit: %d\n", *result);
    } else {
        printf("Cache miss: %d\n", target);
    }

    // 插入数据 4,导致数据 1 被淘汰
    printf("Inserting: 4 (This should evict 1)\n");
    lru_cache_insert(cache, d, NULL, NULL);

    // 再次查找数据 1:应该未命中
    target = 1;
    result = lru_cache_lookup(cache, &target);
    if (result) {
        printf("Cache hit: %d\n", *result);
    } else {
        printf("Cache miss: %d\n", target);
    }

    // 查找数据 3:应该命中
    target = 3;
    result = lru_cache_lookup(cache, &target);
    if (result) {
        printf("Cache hit: %d\n", *result);
    } else {
        printf("Cache miss: %d\n", target);
    }

    // 释放缓存
    free_lru_cache(cache);

    printf("---- LRU Cache Test Complete ----\n");
    return 0;
}

测试结果:
在这里插入图片描述
这是一个非常简单的测试代码,其实上面的LRU缓存实现是和数据类型无关的,因为我们通过函数指针提供了对数据的操作抽象,例如:
free_elem 回调:
允许用户自定义如何释放节点中的数据,可以适配不同类型的内存管理。
hit_elem 回调:
允许用户自定义数据匹配逻辑,适配任意类型的比较需求。
这些设计使得我们的缓存可以支持任意类型的数据,而不仅仅是局限在int类型。
下面我们来一个自定义数据类型的测试代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lru_cache.h"

// 自定义数据类型:Person
typedef struct {
    char* name;
    int age;
} Person;

// 自定义释放函数:释放 Person 类型的数据
void free_person(void* data) {
    Person* person = (Person*)data;
    printf("[Free] Name = %s, Age = %d\n", person->name, person->age);
    free(person->name); // 释放 name 字符串
    free(person);       // 释放 Person 结构体
}

// 自定义匹配函数:根据姓名匹配 Person
int match_person(void* elem, void* user_data) {
    Person* person = (Person*)elem;
    char* target_name = (char*)user_data;
    return strcmp(person->name, target_name) == 0;
}

// 工具函数:创建 Person 对象
Person* create_person(const char* name, int age) {
    Person* person = malloc(sizeof(Person));
    person->name = strdup(name); // 分配并复制 name
    person->age = age;
    return person;
}

// 测试函数:打印缓存的命中率统计信息
void print_cache_stats(struct lruCache* cache) {
    printf("Cache Stats: Hits = %.0f, Misses = %.0f, Hit Rate = %.2f%%\n",
           cache->hit_count,
           cache->miss_count,
           cache->hit_count / (cache->hit_count + cache->miss_count) * 100.0);
}

int main() {
    printf("---- Comprehensive LRU Cache Test ----\n");

    // 创建容量为 3 的 LRU 缓存
    struct lruCache* cache = new_lru_cache(3, free_person, match_person);

    // 插入数据:Person 结构体
    printf("Inserting: Alice, Bob, Charlie\n");
    lru_cache_insert(cache, create_person("Alice", 25), NULL, NULL);
    lru_cache_insert(cache, create_person("Bob", 30), NULL, NULL);
    lru_cache_insert(cache, create_person("Charlie", 35), NULL, NULL);

    // 查找数据:命中 Bob
    printf("Looking up: Bob\n");
    char* target_name = "Bob";
    Person* result = (Person*)lru_cache_lookup(cache, target_name);
    if (result) {
        printf("[Hit] Found: Name = %s, Age = %d\n", result->name, result->age);
    } else {
        printf("[Miss] Not found: %s\n", target_name);
    }

    // 插入新数据 Dave,触发淘汰最久未使用的数据 Alice
    printf("Inserting: Dave (Evicts Alice)\n");
    lru_cache_insert(cache, create_person("Dave", 40), NULL, NULL);

    // 查找 Alice:未命中
    printf("Looking up: Alice\n");
    target_name = "Alice";
    result = (Person*)lru_cache_lookup(cache, target_name);
    if (result) {
        printf("[Hit] Found: Name = %s, Age = %d\n", result->name, result->age);
    } else {
        printf("[Miss] Not found: %s\n", target_name);
    }

    // 查找 Charlie:命中
    printf("Looking up: Charlie\n");
    target_name = "Charlie";
    result = (Person*)lru_cache_lookup(cache, target_name);
    if (result) {
        printf("[Hit] Found: Name = %s, Age = %d\n", result->name, result->age);
    } else {
        printf("[Miss] Not found: %s\n", target_name);
    }

    // 打印缓存的命中率统计信息
    print_cache_stats(cache);

    // 释放缓存
    printf("Freeing the cache...\n");
    free_lru_cache(cache);

    printf("---- LRU Cache Test Complete ----\n");
    return 0;
}


测试结果:
在这里插入图片描述
上面的LRU缓存代码其实是一个设计精巧、功能全面的缓存实现,具有高度的通用性和灵活性。通过将缓存管理逻辑与数据操作解耦,提供了标准化的接口,包括 元素插入、查找、淘汰、命中统计等功能,同时通过回调函数支持任意数据类型的自定义释放和匹配逻辑。
核心优势总结:
①模块化设计:通过free_elem和hit_elem回调函数,适配不同数据类型,用户无需修改核心代码即可实现各种缓存需求。
②功能丰富:支持 LRU 淘汰策略,自动移除最久未使用的数据。提供查找、插入、条件删除、命中模拟等多种操作接口。统计命中次数和未命中次数,便于分析缓存性能。
③内存管理安全:使用 free_elem 回调释放数据,确保内存不会泄漏。
④易于扩展:代码逻辑清晰,接口简单易用,方便进一步添加功能,如并发支持、过期数据清理等

3.LRU缓存总结

接下来我将通过两个表格来简要描述一下LRU缓存的应用以及优缺点。

3.1 LRU 缓存的应用

应用场景描述作用
操作系统内存管理用于页面置换机制,替换最久未使用的内存页。提高内存利用率,减少磁盘 I/O。
数据库缓存缓存频繁访问的数据,淘汰不常用的数据。提高查询性能,减少查询延迟。
Web 浏览器缓存缓存网页资源(如 HTML、图片、CSS 等),加快访问速度。减少重复下载,提升用户体验。
CDN 内容分发网络缓存热点内容,替换最少访问的资源。减少带宽消耗,加速内容传输。
嵌入式系统管理资源受限设备中的数据缓存。提高执行效率,优化内存占用。
数据流处理与缓存临时缓存大数据处理中间结果,腾出空间以继续处理新的数据。提升处理速度,避免重复计算。

3.2 LRU 缓存的优缺点

类别描述
优点1. 实现简单:逻辑清晰,易于实现。
2. 适应时间局部性:能很好处理最近访问的数据,提高缓存命中率。
3. 广泛适用:适用于多种缓存管理场景,如内存、数据库、浏览器等。
缺点1. 空间开销大:需额外使用链表和哈希表维护数据顺序,增加内存消耗。
2. 性能瓶颈:若未优化,查找和移动数据时间复杂度较高(O(N))。
3. 非最佳策略:在数据访问均匀分布或随机的情况下,命中率较低,效果不佳。
4. 线程安全问题:在多线程环境下,需要额外加锁保护,影响性能。

总结:LRU 缓存在操作系统、数据库、Web 浏览器等场景中具有广泛应用,优点是实现简单、适应时间局部性,但也存在空间开销和性能瓶颈等缺点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/939714.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【视频生成模型】——Hunyuan-video 论文及代码讲解和实操

&#x1f52e;混元文生视频官网 | &#x1f31f;Github代码仓库 | &#x1f3ac; Demo 体验 | &#x1f4dd;技术报告 | &#x1f60d;Hugging Face 文章目录 论文详解基础介绍数据预处理 &#xff08;Data Pre-processing&#xff09;数据过滤 (Data Filtering)数据标注 (Data…

【C++】函数计算题解论

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目描述&#x1f4af;思路解析3.1 函数的递归定义3.2 边界条件控制3.3 记忆化搜索 &#x1f4af;C实现代码&#x1f4af;添加解释&#x1f4af;小结 &#x1f4af;前言 在…

低温高海拔大载重无人机吊运技术详解

低温高海拔大载重无人机吊运技术是一项复杂而先进的技术&#xff0c;它结合了无人机的飞行控制、吊装系统的操作以及特殊环境下的适应性等多个方面。以下是对该技术的详细解析&#xff1a; 一、无人机基础知识与结构特点 低温高海拔大载重无人机通常采用旋翼设计&#xff0c;…

Java设计模式 —— 【结构型模式】适配器模式(类的适配器、对象适配器、接口适配器)详解

文章目录 基本介绍一、类的适配器二、对象适配器三、接口适配器总结 基本介绍 生活中有很多例子&#xff1a; 不同国家的插座接口不同&#xff0c;需要转换器&#xff1b;家用电源220V&#xff0c;手机只接受5V充电&#xff0c;需要转换器&#xff1b;读卡器&#xff0c;拓展…

系列2:基于Centos-8.6Kubernetes 集成GPU资源信息

每日禅语 自省&#xff0c;就是自我反省、自我检查&#xff0c;自知己短&#xff0c;从而弥补短处、纠正过失。佛陀强调自觉觉他&#xff0c;强调以达到觉行圆满为修行的最高境界。要改正错误&#xff0c;除了虚心接受他人意见之外&#xff0c;还要不忘时时观照己身。自省自悟之…

leetcode17:电话号码的字母组合

给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输入&#xff1a;digits "23" 输出&#…

OpenHarmony-3.HDF Display子系统(6)

Display 子系统 1.Display驱动模型介绍 当前操作系统和 SOC 种类繁多&#xff0c;各厂商的显示屏器件也各有不同&#xff0c;随之针对器件的驱动代码也不尽相同&#xff0c;往往是某一款器件驱动&#xff0c;只适用于某单一内核系统或 SOC&#xff0c;如果要迁移到其他内核或者…

AQS源码学习

一、park/unpark阻塞唤醒线程 LockSupport是JDK中用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞&#xff0c;可以指定任何线程进行唤醒&#xff0c;并且不用担心阻塞和唤醒操作的顺序&#xff0c;但要注意连续多次唤醒的效果和一次唤醒是一样的。JDK并发包下…

GUI07-学工具栏,懂MVC

MVC模式&#xff0c;是天底下编写GUI程序最为经典、实效的一种软件架构模式。当一个人学完菜单栏、开始学习工具栏时&#xff0c;就是他的一生中&#xff0c;最适合开始认识 MVC 模式的好时机之一。这节将安排您学习&#xff1a; Model-View-Controller 模式如何创建工具栏以及…

C++----类与对象(中篇)

引言 以C语言栈的实现为例&#xff0c;在实际开发中&#xff0c;我们可能会遇到以下两个问题&#xff1a; 1.初始化和销毁管理不当&#xff1a;C语言中的栈实现通常需要手动管理内存&#xff08;如使用malloc和free&#xff09;&#xff0c;这导致初始化和销毁栈时容易出错或…

linux打包qt程序

Linux下Qt程序打包_linuxdeployqt下载-CSDN博客 Linux/Ubuntu arm64下使用linuxdeployqt打包Qt程序_linuxdeployqt arm-CSDN博客 本篇文章的系统环境是 : 虚拟机ubuntu18.04 用下面这个qmake路径 进行编译 在 ~/.bashrc 文件末尾&#xff0c;qmake目录配置到文件末尾 将上图中…

气象与旅游之间的关系,如果借助高精度预测提高旅游的质量

气象与旅游之间存在密切的关系,天气条件直接影响旅游者的出行决策、旅游体验和安全保障。通过高精度气象预测技术,可以有效提升旅游质量,为游客和旅游行业带来显著的优势。 1. 提高游客出行决策效率 个性化天气服务:基于高精度气象预测,旅游平台可以提供个性化的天气预报服…

华为OD --- 靠谱的车

华为OD --- 靠谱的车 题目OJ用例独立实现思路源码 参考实现思路源码实现 题目 OJ用例 测试用例case 独立实现 思路 独立实现的思路比较简单,直接建一个长度为N的数组,然后找出index中不包含4的项数即可 源码 const rl require("readline").createInterface({ …

可视化平台FineReport的安装及简单使用

1. FineReport产品 FineReport介绍 FineReport报表软件是一款纯Java编写的、集数据展示(报表)和数据录入(表单)功能于一身的企业级web报表工具&#xff0c;它专业、简捷、灵活的特点和无码理念&#xff0c;仅需简单的拖拽操作便可以设计复杂的中国式报表&#xff0c;搭建数据决…

OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用

目录 一&#xff0c;分发器和拦截器 二&#xff0c;分发器处理异步请求 1.分发器处理入口 2.分发器工作流程 3.分发器中的线程池设计 三&#xff0c;分发器处理同步请求 四&#xff0c;拦截器处理请求 1.责任链设计模式 2.拦截器工作原理 3.OkHttp五大拦截器 一&#…

Nginx主要知识点总结

1下载nginx 到nginx官网nginx: download下载nginx&#xff0c;然后解压压缩包 然后双击nginx.exe就可以启动nginx 2启动nginx 然后在浏览器的网址处输入localhost&#xff0c;进入如下页面说明nginx启动成功 3了解nginx的配置文件 4熟悉nginx的基本配置和常用操作 Nginx 常…

概率论得学习和整理27:关于离散的数组 随机变量数组的均值,方差的求法3种公式,思考和细节。

目录 1 例子1&#xff1a;最典型的&#xff0c;最简单的数组的均值&#xff0c;方差的求法 2 例子1的问题&#xff1a;例子1只是1个特例&#xff0c;而不是普遍情况。 2.1 例子1各种默认假设&#xff0c;导致了求均值和方差的特殊性&#xff0c;特别简单。 2.2 我觉得 加权…

初学stm32 --- 时钟配置

目录 stm32时钟系统 时钟源 &#xff08;1&#xff09; 2 个外部时钟源&#xff1a; &#xff08;2&#xff09;2 个内部时钟源&#xff1a; 锁相环 PLL PLLXTPRE&#xff1a; HSE 分频器作为 PLL 输入 (HSE divider for PLL entry) PLLSRC&#xff1a; PLL 输入时钟源 (PL…

Latex+VsCode+Win10搭建

最近在写论文&#xff0c;overleaf的免费使用次数受限&#xff0c;因此需要使用本地的形式进行编译。 安装TEXLive 下载地址&#xff1a;https://mirror-hk.koddos.net/CTAN/systems/texlive/Images/ 下载完成直接点击iso进行安装操作。 安装LATEX Workshop插件 设置VsCode文…

深度学习之目标检测篇——残差网络与FPN结合

特征金字塔多尺度融合特征金字塔的网络原理 这里是基于resnet网络与Fpn做的结合&#xff0c;主要把resnet中的特征层利用FPN的思想一起结合&#xff0c;实现resnet_fpn。增强目标检测backone的有效性。代码实现如下&#xff1a; import torch from torch import Tensor from c…