本地缓存库分析(一):golang-lru

news2024/10/27 10:11:27

在这里插入图片描述

文章目录

  • 本地缓存概览
  • golang-lru
  • 标准lru
    • lru的操作
      • Put
      • Get
  • 2q:冷热分离lru
    • Put
    • Get
  • expirable_lru:支持过期时间的lru
    • Put
    • Get
    • 过期
  • 总结

本地缓存概览

在业务中,一般会将极高频访问的数据缓存到本地。以减少网络IO的开销,下游服务的压力,提高性能

一般来说能放到本地的数据需要满足下面两个限制:

  1. 数据量不是非常大:数据量大了本地内存撑不住
  2. 一致性,时效性要求不是非常高:毕竟多个服务的本地缓存很难做到同步更新,及时更新

如果用go自带的map实现本地缓存,大概有两种实现方式:

  1. sync.Map
  2. map + mutex.RWLock

但有以下缺点:

  1. 锁竞争严重
  2. 大量缓存写入,导致gc标记阶段占用cpu多
  3. 内存占用不可控
  4. 不支持缓存按时效性淘汰
  5. 不支持缓存过期
  6. 缓存数据可以被污染:如果缓存的V是指针,那么业务修改了V的某个值为当前请求用户自己的值,在缓存中的V就被污染了

本系列要介绍的开源缓存库如何解决上述问题?

  1. 大map拆分成多个小map,每个小map使用各自的锁

  2. 零GC:

    1. 使用堆外内存,不把对象放到堆上,自然不会被gc扫描。但要注意手动管理,需要及时释放内存

    2. map的非指针优化

      1. 如果kv都没有指针,不会扫描map。注意常用作key的string类型含有指针,会被gc扫描
      2. 将hash值作为key,value在底层数组中的offset作为value,这样KV都是int,就不会被GC扫描了
  3. 内存占用可控

    1. 初始化时制定好底层数组的容量,数据写满时会覆写,这样永远不会超过容量
    2. 或者指定好最多能放多少KV对,但如果V大小不一,极端情况下内存占用会很大
  4. 支持缓存按时效性淘汰,例如使用LRU算法

  5. 支持数据过期

    1. 某些库在后台启定时任务,定时清理过期的KV
    2. 某些库会在Get时,惰性检查KV是否过期
  6. 避免缓存污染:存储Value序列化后的字节数组,而不是指针

    1. 但cpu开销会增大,每次写入缓存都要经过序列化,每次从缓存读都要经过反序列化。内存开销也变大,每次读都相当于拷贝一份出来
    2. 也就是用性能换取安全性

golang-lru

本文阅读源码:https://github.com/hashicorp/golang-lru,版本:v2.0.7

该库提供了3种LRU的实现:

  • lru:标准lru

  • 2q:类似mysql的buffer pool,分为冷数据热数据两部分。如果某对KV只被添加到缓存中,而没有被查询,那么只会待在冷数据区域直到被淘汰,而不会占用热数据的空间

    • 为啥要冷热分离?考虑这样一种场景:假设缓存中有10条热点数据,突然有用户往缓存中写10条冷数据,因为容量不够,要淘汰掉所有的热数据。如果之后也不查这些冷数据,而是继续查之前的热数据。产生的后果是所有热数据都会cacheMiss
    • 如果应用了冷热分离机制,这些冷数据只会写到冷数据区,然后在冷数据区被淘汰,而不会占用热数据的空间,避免了大量热数据的cacheMiss
  • expirable_lru:支持过期时间的lru


标准lru

数据结构:包含一个双向链表和hash表
在这里插入图片描述

type LRU[K comparable, V any] struct {
	// 容量
	size int
	// entry越靠近头部,越新
	evictList *internal.LruList[K, V]
    // hash表
	items     map[K]*internal.Entry[K, V]
	onEvict   EvictCallback[K, V]
}

LruList就是个带哨兵头节点root的双向链表,每个节点是Entry结构。哈希表items的value也是Entry机构

那么真正的头节点是root.Next

尾节点是root.Prev

type LruList[K comparable, V any] struct {
	// 哨兵entry
	root Entry[K, V]

	// 已经放了多少entry
	len int
}

entry结构如下:

type Entry[K comparable, V any] struct {
    // 前后指针
	next, prev *Entry[K, V]

	// 属于哪个list,主要用于遍历链表时,在lru算法中没啥用
	list *LruList[K, V]

	Key K

	Value V
}

初始化时root自己先形成一个环:

func (l *LruList[K, V]) Init() *LruList[K, V] {
	l.root.next = &l.root
	l.root.prev = &l.root
	l.len = 0
	return l
}

下面介绍一些在LRU算法中会用到的小方法

获取最后一个节点:

func (l *LruList[K, V]) Back() *Entry[K, V] {
	if l.len == 0 {
		return nil
	}
	return l.root.prev
}

将entry添加到头部:

func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] {
	l.lazyInit()
	return l.insertValue(k, v, time.Time{}, &l.root)
}
func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] {
	return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at)
}

就是普通的双链表操作,将e插到at的后面

func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] {
	e.prev = at
	e.next = at.next
	e.prev.next = e
	e.next.prev = e
	e.list = l
	l.len++
	return e
}

将entry移动到头部:

func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) {
	if e.list != l || l.root.next == e {
		return
	}

    l.move(e, &l.root)
}

move方法:

func (l *LruList[K, V]) move(e, at *Entry[K, V]) {
	if e == at {
		return
	}
	 
	// 先将e从list删除
	e.prev.next = e.next
	e.next.prev = e.prev

	// 再将e插到at后面
	e.prev = at
	e.next = at.next
	e.prev.next = e
	e.next.prev = e
}

lru的操作

主要看看GetPut这两大流程

原则:每次操作完,都要将KV所在的entry移动到list头部,表示该entry实效性最好,最不应该过期


Put

func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
	// 如果已存在,将entry移到链表头部,更新value
	if ent, ok := c.items[key]; ok {
		c.evictList.MoveToFront(ent)
		ent.Value = value
		return false
	}

	// 新元素,加入头部
	ent := c.evictList.PushFront(key, value)
	c.items[key] = ent

	evict := c.evictList.Length() > c.size
	// 如果容量超了,需要移除最老的
	if evict {
		c.removeOldest()
	}
	return evict
}

removeOldest:删除最老的entry

func (c *LRU[K, V]) removeOldest() {
    // 找到列表末尾的entry
	if ent := c.evictList.Back(); ent != nil {
		c.removeElement(ent)
	}
}

// 把entry从链表和map中删除
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
	c.evictList.Remove(e)
	delete(c.items, e.Key)
	if c.onEvict != nil {
		c.onEvict(e.Key, e.Value)
	}
}

Get

如果key存在,将key所在的entry移动到list头部

func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
	if ent, ok := c.items[key]; ok {
		c.evictList.MoveToFront(ent)
		return ent.Value, true
	}
	return
}

2q:冷热分离lru

2q在用了3个lru

  • recent:保存冷数据,默认容量为size的1/4
  • frequent:保存热数据,默认容量为size的3/4
  • recentEvict:保存最近从冷数据中被删除的key,默认容量为size的1/2

数据加到缓存时,首先被添加到冷数据区,如果后续没有操作,就会在冷数据区被淘汰。如果在还没被淘汰时执行了Put或Get,就会提升到热数据区

在Put时,如果某个key在冷热数据都没有,但在recentEvict中有,说明是最近被删除的,也当做热数据处理,加到冷数据区

在这里插入图片描述

type TwoQueueCache[K comparable, V any] struct {
	size int
	// size * 0.25
	recentSize  int
    // 0.25
	recentRatio float64
    // 0.5
	ghostRatio  float64

	// 保存冷数据
	recent simplelru.LRUCache[K, V]
	// 正常热数据
	frequent simplelru.LRUCache[K, V]
	// 最近从冷数据删除的key
	recentEvict simplelru.LRUCache[K, struct{}]
	lock        sync.RWMutex
}

构造方法,就是初始化3个lru,以及每个lru的容量


const (
    // 默认冷数据的容量 = size * 0.25
	Default2QRecentRatio = 0.25
    // 默认被删除数据的容量 = size * 0.5
	Default2QGhostEntries = 0.50
)


func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) {
	return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries)
}

func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) {
	recentSize := int(float64(size) * recentRatio)
	evictSize := int(float64(size) * ghostRatio)

    // 初始化3个lru
	recent, err := simplelru.NewLRU[K, V](size, nil)
	if err != nil {
		return nil, err
	}
	frequent, err := simplelru.NewLRU[K, V](size, nil)
	if err != nil {
		return nil, err
	}

	recentEvict, err := simplelru.NewLRU[K, struct{}](evictSize, nil)
	if err != nil {
		return nil, err
	}

	c := &TwoQueueCache[K, V]{
		size:        size,
		recentSize:  recentSize,
		recentRatio: recentRatio,
		ghostRatio:  ghostRatio,
		recent:      recent,
		frequent:    frequent,
		recentEvict: recentEvict,
	}
	return c, nil
}

重点还是看Put和Get


Put

流程为:

  1. 如果key在热数据中有,那就在热数据的lru中执行Put,更新其value,返回
  2. 如果在冷数据中有,那么本次不是第一次操作,说明该key不再是冷数据了,将其移动到热数据的lru中,返回
  3. 到这一步说明在冷热两个lru中都没有,再看看recentEvict中有没有,如果有,说明是最近才从冷数据被删除的,那么也算作是热数据,在热数据lru中新增
  4. 否则就在冷数据lru中新增
func (c *TwoQueueCache[K, V]) Add(key K, value V) {
	c.lock.Lock()
	defer c.lock.Unlock()

	// 如果在热数据中有,在热数据的lru中执行Put
	if c.frequent.Contains(key) {
		c.frequent.Add(key, value)
		return
	}

	// 如果在冷数据中有,移动到frequent中
	if c.recent.Contains(key) {
		c.recent.Remove(key)
		c.frequent.Add(key, value)
		return
	}

	// 在两个lru中都没有,但最近移除过这个key,加到frequent中
	if c.recentEvict.Contains(key) {
		c.ensureSpace(true)
		c.recentEvict.Remove(key)
		c.frequent.Add(key, value)
		return
	}

	// 否则加到recent中
	c.ensureSpace(false)
	c.recent.Add(key, value)
}

当需要新增时,需要确保容量足够,如果容量超了,需要淘汰老数据,给新数据腾位置

ensureSpace方法干这个活,淘汰规则为:

  1. 如果recent和frequent的len加起来不够size,判定为还有容量,不淘汰

    1. 也就是说,在容量没满时,冷热数据区分别都可以用到size个空间,有很大的灵活性
  2. 否则看冷数据区frequent有没有超过容量限制,超过了就从frequent中淘汰一个

  3. 否则从热数据区中淘汰一个

func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) {
	// 如果还有空间,返回
	recentLen := c.recent.Len()
	freqLen := c.frequent.Len()
	if recentLen+freqLen < c.size {
		return
	}

	/**
	recent超过了限制, 从recent移除最老的entry,将key加到recentEvict中
	*/
	if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
		k, _, _ := c.recent.RemoveOldest()
		c.recentEvict.Add(k, struct{}{})
		return
	}

	// 否则就是frequent超过限制了,从frequent中移除最老的entry
	c.frequent.RemoveOldest()
}

Get

  1. 先看热数据有没有该key,如果有返回对应的value
  2. 再看冷数据有没有,如果有,说明本次不是第一次操作该key,将其提升到热数据中
  3. 否则在冷热数据中都没有,返回空
func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) {
	c.lock.Lock()
	defer c.lock.Unlock()

    // 先看热数据有没有
    if val, ok := c.frequent.Get(key); ok {
		return val, ok
	}

	// 在看冷数据有没有
	if val, ok := c.recent.Peek(key); ok {
		c.recent.Remove(key)
		c.frequent.Add(key, val)
		return val, ok
	}

	// No hit
	return
}

expirable_lru:支持过期时间的lru

其结构就是在标准LRU的基础上,增加过期时间ttl和过期桶(固定为100个)
所有KV都应用相同的过期时间ttl

每次Put后,会把key加到最新的过期桶中
后台有定时任务,每ttl/100时间执行一次,把即将过期的桶nextCleanupBucket 中的数据清空

在这里插入图片描述

type LRU[K comparable, V any] struct {
    // 标准LRU结构
	size      int
	evictList *internal.LruList[K, V]
	items     map[K]*internal.Entry[K, V]
	onEvict   EvictCallback[K, V]

	mu   sync.Mutex
    // LRU中的所有kv都用这个过期时间
	ttl  time.Duration
	done chan struct{}

	// 存储所有过期的key,
	buckets []bucket[K, V]
	// 下次要清除的bucket索引
	nextCleanupBucket uint8
}

bucket定义如下

type bucket[K comparable, V any] struct {
    // 所有过期的key
	entries     map[K]*internal.Entry[K, V]
    // enteied中的所有key,最晚在啥时候过期
	newestEntry time.Time
}

Put

在标准LRU的基础上,新增了对过期桶的操作

func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	now := time.Now()

	// key存在
	if ent, ok := c.items[key]; ok {
		c.evictList.MoveToFront(ent)
        // 将entry从过期桶移除
		c.removeFromBucket(ent)
		ent.Value = value
        
		ent.ExpiresAt = now.Add(c.ttl)
        // 加入最新的过期桶
		c.addToBucket(ent)
		return false
	}

    // key不存在
	ent := c.evictList.PushFrontExpirable(key, value, now.Add(c.ttl))
	c.items[key] = ent
	c.addToBucket(ent)

    // 容量超了,移除最老的元素
	evict := c.size > 0 && c.evictList.Length() > c.size
	if evict {
		c.removeOldest()
	}
	return evict
}

看看怎么将entry加入过期桶:

func (c *LRU[K, V]) addToBucket(e *internal.Entry[K, V]) {
	// 加到nextCleanupBucket-1对应的bucket里,也就是最新的bucket
	bucketID := (numBuckets + c.nextCleanupBucket - 1) % numBuckets
	e.ExpireBucket = bucketID
	c.buckets[bucketID].entries[e.Key] = e

	// 更新桶中最新的entry过期时间
	if c.buckets[bucketID].newestEntry.Before(e.ExpiresAt) {
		c.buckets[bucketID].newestEntry = e.ExpiresAt
	}
}

解释下为啥加到下标为nextCleanupBucket-1的桶里:nextCleanupBucket为即将失效的桶,那么nextCleanupBucket-1就是在当前时刻来说,最晚失效的桶


Get

在标准LRU的Get流程上,多了一步校验key是否过期

func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
  
	var ent *internal.Entry[K, V]
	if ent, ok = c.items[key]; ok {
		// 校验是否过期
		if time.Now().After(ent.ExpiresAt) {
			return value, false
		}
		c.evictList.MoveToFront(ent)
		return ent.Value, true
	}
	return
}

过期

缓存数据怎么实现过期呢?在初始化LRU时,起了后台任务:

go func(done <-chan struct{}) {
    ticker := time.NewTicker(res.ttl / numBuckets)
    defer ticker.Stop()
    for {
        select {
        case <-done:
            return
        case <-ticker.C:
            res.deleteExpired()
        }
    }
}(res.done)

每隔一段时间执行deleteExpired方法:

  1. 准备将下标为nextCleanupBucket的中的所有KV过期
  2. sleep直到时间到newestEntry,因为桶中最晚过期的key在这个时候,不能提前过期
  3. 将该桶中所有KV删除
  4. 推进nextCleanupBucket,让nextCleanupBucket++
func (c *LRU[K, V]) deleteExpired() {
	c.mu.Lock()
    // 
	bucketIdx := c.nextCleanupBucket
	timeToExpire := time.Until(c.buckets[bucketIdx].newestEntry)
	// sleep直到newestEntry到来
	if timeToExpire > 0 {
		c.mu.Unlock()
		time.Sleep(timeToExpire)
		c.mu.Lock()
	}

	// 将里面所有kv删除
	for _, ent := range c.buckets[bucketIdx].entries {
		c.removeElement(ent)
	}

	// 推进nextCleanupBucket
	c.nextCleanupBucket = (c.nextCleanupBucket + 1) % numBuckets
	c.mu.Unlock()
}

总结

最后看看golang-lru解决了哪些原生缓存的问题:

问题解决
锁竞争严重没有解决,只有一把大锁,锁竞争依然严重
大量缓存写入,导致gc标记阶段占用cpu多没有解决
内存占用不可控有改善,在KV个数的层面可用,在总内存占用量的层面依然不可用
不支持缓存按时效性淘汰解决了,支持按LRU算法淘汰
不支持缓存过期解决了,expirable_lru支持
缓存数据可以被污染没有解决,还是存指针

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【OpenAI】第五节(图像生成)利用 OpenAI 的 DALL·E 实现自动化图像生成:从文本到图像的完整教程

引言 OpenAI 推出的 DALLE 工具因其能够生成令人惊叹的艺术作品而备受瞩目。DALLE 不仅能够生成静态图像&#xff0c;还能根据用户的需求进行风格化处理&#xff0c;创造出独特的艺术作品。通过 OpenAI 的 API&#xff0c;你可以轻松将 DALLE 的强大功能集成到你的 Python 程序…

基于SSM的智慧篮球馆预约系统

前言 近些年&#xff0c;随着中国经济发展&#xff0c;人民的生活质量逐渐提高&#xff0c;对网络的依赖性越来越高&#xff0c;通过网络处理的事务越来越多。随着智慧篮球馆预约的常态化&#xff0c;如果依然采用传统的管理方式&#xff0c;将会为工作人员带来庞大的工作量&a…

Linux中C/C++程序编译过程与动静态链接库概述

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

qt QMainWindow详解

一、概述 QMainWindow继承自QWidget&#xff0c;并提供了一个预定义的布局&#xff0c;将窗口分成了菜单栏、工具栏、状态栏和中央部件区域。这些区域共同构成了一个功能丰富的主窗口&#xff0c;使得应用程序的开发更加简单和高效。 二、QMainWindow的常用组件及功能 菜单栏&…

【Java网络编程】从套接字(Socket)概念到UDP与TCP套接字编程

目录 网络编程 1.socket套接字 2.udp数据报套接字编程 DatagramSocket API DatagramPacket API Java基于UDP实现客户端-服务器代码实例 3.tcp流套接字编程 ServerSocket API Socket API TCP中的长短连接 Java基于TCP客户端-服务器代码实例 网络编程 1.socket套接字 S…

云对象存储进阶

《使用Minio搭建文件服务器》一文对minio作了简单的介绍&#xff0c;本文为进阶学习。 1.对象存储产品介绍 目前市场上流行各种对象存储服务&#xff0c;诸如以下&#xff1a; Amazon S3&#xff1a;亚马逊提供的服务&#xff0c; 是市场上最成熟的产品&#xff0c;拥有最大的…

ATom:2016-2018 年沿飞行轨迹的 CAM-chem/CESM2 模型输出

目录 简介 摘要 代码 引用 网址推荐 知识星球 机器学习 ATom: CAM-chem/CESM2 Model Outputs Along Flight Tracks, 2016-2018 ATom&#xff1a;2016-2018 年沿飞行轨迹的 CAM-chem/CESM2 模型输出 简介 该数据集包含沿 ATom 飞行轨迹的 CAM-chem&#xff08;带化学的…

[ARM-2D 专题]5 MDK编译器一个旧版本-Ofast优化bug的问题及解决办法

最近开始大量基于ARM-2D开发应用项目&#xff0c;为了达到最佳性能&#xff0c;我们使用了编译器的许多特殊技能&#xff0c;其中就包含了-Ofast优化&#xff0c;很不幸&#xff0c;一不小心踩坑了。 案发情况如下&#xff1a; 使用的MDK版本5.36&#xff0c;编译器6.16 优化选…

在GeoTools中的Shapefile属性表读取效率之Shp与Dbf对比

目录 前言 一、POI测试数据简介 1、选用的POI数据 2、关于数据的属性数据 二、属性数据读取的两种方式实现 1、基于DbaseFileReader的读取 2、基于SimpleFeatureSource的读取 三、实际运行对比 1、内存和CPU占用情况 2、运行耗时情况 四、总结 前言 众所周知&#x…

【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper+代码——交叉注意力(Cross-Attention)

【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper代码——交叉注意力&#xff08;Cross-Attention&#xff09; 【深度学习中的注意力机制10】11种主流注意力机制112个创新研究paper代码——交叉注意力&#xff08;Cross-Attention&#xff09; 文章目录 【…

‌Spring MVC的主要组件有哪些?

前言 SpringMVC的核心组件包括DispatcherServlet、Controller、HandlerMapping、HandlerAdapter、ViewResolver、ModelAndView等&#xff0c;它们协同工作以支持基于MVC架构的Web应用程序开发。这些组件使得开发人员能够以一种声明式和模块化的方式构建Web应用程序&#xff0c…

小程序开发实战:PDF转换为图片工具开发

目录 一、开发思路 1.1 申请微信小程序 1.2 编写后端接口 1.3 后端接口部署 1.4 微信小程序前端页面开发 1.5 运行效果 1.6 小程序部署上线 今天给大家分享小程序开发系列&#xff0c;PDF转换为图片工具的开发实战&#xff0c;感兴趣的朋友可以一起来学习一下&#xff01…

ECharts饼图-基础南丁格尔玫瑰图,附视频讲解与代码下载

引言&#xff1a; 在数据可视化的世界里&#xff0c;ECharts凭借其丰富的图表类型和强大的配置能力&#xff0c;成为了众多开发者的首选。今天&#xff0c;我将带大家一起实现一个饼图图表&#xff0c;通过该图表我们可以直观地展示和分析数据。此外&#xff0c;我还将提供详…

一、在cubemx下RTC配置调试实例测试

一、rtc的时钟有lse提供。 二、选择rtc唤醒与闹钟功能 内部参数介绍 闹钟配置 在配置时间时&#xff0c;注意将时间信息存储起来&#xff0c;防止复位后时间重新配置。 if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0)! 0x55AA)//判断标志位是否配置过&#xff0c;没有则进…

qt EventFilter用途详解

一、概述 EventFilter是QObject类的一个事件过滤器&#xff0c;当使用installEventFilter方法为某个对象安装事件过滤器时&#xff0c;该对象的eventFilter函数就会被调用。通过重写eventFilter方法&#xff0c;开发者可以在事件处理过程中进行拦截和处理&#xff0c;实现对事…

WSL2 Ubuntu22.04编译安装LLVM

前提 这两天因为工作需要&#xff0c;要编译一个Debug版本的llvm。这里对编译安装过程进行一个简单的记录&#xff0c;同时也记录下这个过程中遇到的几个问题。 下载源码并编译 有关llvm编译安装的官方文档在这里。 从git仓库clone llvm的源码。 git clone https://github.c…

FPGA搭建PCIE3.0通信架构简单读写测试,基于XDMA中断模式,提供3套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的PCIE方案本博客方案的PCIE2.0版本 3、PCIE基础知识4、工程详细设计方案工程设计原理框图XDMA配置及使用XDMA中断模块数据缓存架构用户逻辑Windows版本XDMA驱动安装Linux版本XDMA驱动安装测试应用程序工程源码架构PCIE上板…

电磁场-Laplace算子与冲激函数的关系

csdn重新打一遍公式太麻烦了。欢迎转到我的知乎账号上查阅原版文章&#xff0c;也可后台私信我发送原版PDF或者markdown。 电磁场-Laplace算子与冲激函数的关系 - 知乎 下面的文章是一张超大的图片。

论1+2+3+4+... = -1/12 的不同算法

我们熟知自然数全加和&#xff0c; 推导过程如下&#xff0c; 这个解法并不难&#xff0c;非常容易看懂&#xff0c;但是并不容易真正理解。正负交错和无穷项计算&#xff0c;只需要保持方程的形态&#xff0c;就可以“预知”结果。但是这到底说的是什么意思&#xff1f;比如和…

C++扑克牌(poker)2024年CSP-J认证第二轮第一题 CCF信息学奥赛C++ 中小学初级组 第二轮真题解析

目录 C扑克牌&#xff08;poker&#xff09; 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、运行结果 五、考点分析 六、推荐资料 C扑克牌&#xff08;poker&#xff09; 2024年CSP-J认证第二轮第一题 一、题目要求 1、编程实现 小 P 从同学…