1.C++-⾼性能内存池

1.C+±⾼性能内存池

1. 项⽬介绍

这个项⽬做的是什么?

当前项⽬是实现⼀个⾼并发的内存池,他的原型是google的⼀个开源项⽬tcmalloc,tcmalloc全称 Thread-Caching Malloc,即线程缓存的malloc,实现了⾼效的多线程内存管理,⽤于替代系统的内 存分配相关的函数(malloc、free)。

这个项⽬是把tcmalloc最核⼼的框架简化后拿出来,模拟实现出⼀个⾃⼰的⾼并发内存池,⽬的 就是学习tcamlloc的精华,这种⽅式有点类似我们之前学习STL容器的⽅式。但是相⽐STL容器部分, tcmalloc的代码量和复杂度上升了很多

tcmalloc源码:

gitee.com/mirrors/tcmalloc

2. 什么是内存池

2.1 池化技术

所谓"池化技术",就是程序先向系统申请过量的资源,然后⾃⼰管理,以备不时之需。之所以要申 请过量的资源,是因为每次申请该资源都有较⼤的开销,不如提前申请好了,这样使⽤时就会变得⾮ 常快捷,⼤⼤提⾼程序运⾏效率。

在计算机中,有很多使⽤"池"这种技术的地⽅,除了内存池,还有连接池、线程池、对象池等。以 服务器上的线程池为例,它的主要思想是:先启动若⼲数量的线程,让它们处于睡眠状态,当接收到 客⼾端的请求时,唤醒池中某个睡眠的线程,让它来处理客⼾端的请求,当处理完这个请求,线程⼜ 进⼊睡眠状态。

2.2 内存池

内存池是指程序预先从操作系统申请⼀块⾜够⼤内存,此后,当程序中需要申请内存的时候,不是直 接向操作系统申请,⽽是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回 给操作系统,⽽是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。

2.3 内存池主要解决的问题

内存池主要解决的当然是效率的问题,其次如果作为系统的内存分配器的⻆度,还需要解决⼀下内存 碎⽚的问题。那么什么是内存碎⽚呢?

内存碎⽚分为外碎⽚和内碎⽚

普通 malloc/free 频繁申请、释放小块内存,会产生内存碎片,内存池就是用来缓解这个问题。

1、什么是外部碎片(外碎片)

举个生活化例子:

你手里有一整条长条座位:【占】【空】【占】【空】【占】【空】

每一块空位单独看都不大。

现在来了一组人,想要连续 3 个空位。

你数一数所有空位加起来总量是够 3 个位置的,但是空位互相隔开、不连在一起,凑不出一整块连续空间。

放到内存里:

系统有很多零散、不连续的空闲内存块。空闲内存总和充足,但没有一大块连续的空闲内存。当程序申请一块较大连续内存时,分配失败。

这就是外部碎片

关键点:内存本身是空闲的,只是碎片化、不连续

2、什么是内部碎片(内碎片)

继续举例:

电影院买票,规定售票只能按2 个座位一组售卖。

你一个人只需要 1 个座位,但规则强制分给你 2 个相连座位。

你只用 1 座,剩下那 1 个座位被分给你了、别人不能坐,一直闲置。

放到内存场景:

硬件 / 内存池有内存对齐规则,分配内存时必须分配固定粒度大小的块。

程序只需要 50 字节,但是内存分配单位最小是 64 字节。系统直接给你 64 字节。

多出 14 字节已经划给你的内存块里,别人拿不走,你自己也用不上,白白浪费。

关键点:这块内存已经被分配出去了,包裹在已占用内存内部,闲置浪费

3、两者核心区别总结
  • 外碎片:空闲内存,但是四分五裂不连续,拼不出大块;
  • 内碎片:内存已经分给进程了,块内部多出一小截空间,永久闲置。

相信各位大佬,到这里可以很容易理解什么是内存池了:

普通动态分配:频繁申请释放 → 产生大量外部碎片

内存池:提前一次性申请一大块连续内存,自己管理里面空间,减少向操作系统频繁申请释放,大幅降低外部碎片。

但内存池为了对齐、按固定块划分,很容易产生内部碎片(要一小块,强制给一整块,剩余空间浪费),这也是后续项目里能直观观察到的现象。

2.4 malloc

C/C++中我们要动态申请内存都是通过malloc去申请内存,但是我们要知道,实际我们不是直接去堆 获取内存的, ⽽malloc就是⼀个内存池。

malloc()相当于向操作系统"批发"了⼀块较⼤的内存空间,然后"零 售"给程序⽤。当全部"售完"或程序有⼤量的内存需求时,再根据实际需求向操作系统"进货"。

malloc的实现⽅式有很多种,⼀般不同编译器平台⽤的都是不同的。⽐如windows的vs系列⽤的微软 ⾃⼰写的⼀套,linuxgcc⽤的glibc中的ptmalloc。

下⾯有⼏篇关于这块的⽂章,⼤概可以去简单看看 了解⼀下,关于ptmalloc,有兴趣⼤家可以去看看他的实现细节。

一文了解,Linux内存管理,malloc、free 实现原理 - 知乎

malloc的底层实现(pcmalloc):

https://blog.csdn.net/z_ryan/article/details/79950737

3. 设计一个定长内存池

作为程序员(C/C++)我们知道申请内存使⽤的是malloc,malloc其实就是⼀个通⽤的⼤众货,什么场景 下都可以⽤,但是什么场景下都可以⽤就意味着什么场景下都不会有很⾼的性能

下⾯我们就先来设 计⼀个定⻓内存池做个开胃菜,当然这个定⻓内存池在我们后⾯的⾼并发内存池中也是有价值的,所 以学习他⽬的有两层,先熟悉⼀下简单内存池是如何控制的,第⼆他会作为我们后⾯内存池的⼀个基础组件。

3.1 设计的基本流程图

3.1 申请空间New()的实现

申请 New ()

  1. 优先查自由链表_freeList:有闲置内存直接拿(不用向系统申请,速度最快)
  2. 没有闲置块:检查预分配大块内存剩余空间
    • 空间不足:malloc 一次性申请 128KB 大内存
    • 从大块内存切出一块裸地址
  3. 统一执行定位 new:在裸内存上调用构造函数,生成合法对象返回

代码如下:

复制代码
// 功能:从内存池里申请一个T类型的对象,返回对象指针
T* New()
{
    T* obj = nullptr;  // 最终要返回的对象指针,先初始化为空

    // ---------------- 第一步:优先复用「别人用完还回来的」内存块 ----------------
    // _freeList 叫自由链表,串着所有回收回来的空闲内存块
    if (_freeList)
    {
        // 取出链表第一个节点里存的「下一个节点的地址」
        // 通俗理解:每个空闲块的前几个字节,我们都用来存下一个空闲块的地址,像链条一样串起来
        void* next = *(void**)_freeList;

        obj = (T*)_freeList;   // 把链表第一个块拿出来,给用户当对象用
        _freeList = next;      // 链表头往后挪一位,指向第二个节点
    }
    // ---------------- 第二步:没有回收块,就从大块内存里切一块 ----------------
    else
    {
        // 先检查:剩下的大块内存,够不够分一个对象出来
        if (_remainBytes < sizeof(T))
        {
            // 不够的话,就向系统一次性申请一大块内存(128KB = 128*1024字节)
            // 一次性申请大块,比一次次申请小块快很多,这是对象池提速的核心之一
            _memory = (char*)malloc(128 * 1024);
            if (_memory == nullptr)
            {
                throw std::bad_alloc();  // 申请失败就抛异常
            }
            _remainBytes = 128 * 1024; // 更新剩余内存大小为刚申请的整块大小
        }

        obj = (T*)_memory;  // 从大块内存的开头,切出一块当对象

        // 计算实际分配大小:如果对象比指针还小,就按指针大小分配
        // 原因:回收的时候,每个块至少要能存下一个指针(用来串链表),不然放不下
        size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

        _memory += objSize;      // 大块内存指针往后挪,相当于切掉了刚分出去的那块
        _remainBytes -= objSize; // 剩余内存减去分出去的大小
    }

    // ---------------- 第三步:在内存上构造对象 ----------------
    // 定位new语法:在已经申请好的内存地址上,主动调用T的构造函数
    // 为什么要这一步?因为前面只拿到了裸内存,还没初始化对象,成员值都是乱的
    new(obj)T;

	
    return obj;
}

小编中午刚睡醒,一时忘记了定位new的语法格式,顺便复习一下吧。

定位new的语法格式:

复制代码
new(已经分配好的内存地址) 类型(构造参数);

① 无参构造

复制代码
new(obj) T;

等价:调用T()默认构造

② 带参数构造

如果类有带参构造函数

复制代码
struct A
{
    A(int x){}
};

char buf[100];
A* p = (A*)buf;
new(p) A(100); // 在p地址调用A(int)构造函数

③ C++11 及以上支持初始化列表

复制代码
new(p) A{100};

顺便来回顾以下定位new和普通的new的区别:

普通 new T()

  1. 向操作系统堆申请内存
  2. 在这块内存调用构造函数

定位 new new(ptr) T

只做第 2 件事!不申请内存! 内存你自己提前准备好。

众所周知,new和delete是一对天赐良缘的伴侣。好好好,既然new都复习了,那我顺手讲一下delete吧

  1. 普通new当然可以配合普通delete(成对使用)用来防止内存泄漏
  2. 但是定位 new分配出来的对象,绝对不能用普通 delete 释放!

普通delete p会做两件事:

  1. 调用析构函数
  2. 释放堆内存

但内存池的内存是我们统一管理的,不能交给系统释放,所以要手动拆分:

比如我代码里的:

复制代码
// 1.手动调用析构,销毁对象(对应定位new的反向操作)
obj->~T();
// 2.内存不还给系统,回收进自由链表,后续重复使用

生命周期配对:

new(ptr) T; --obj->~T();

3.2 Delete()的使用

哈哈哈哈,上面好像把delete该讲的都讲了,没事没事,这次就当奖励了

3.2.1delete执行流程如下:

用户用完调用 Delete(obj)

  • obj->~T():销毁对象,对象内部资源释放
  • 这块内存被挂回_freeList空闲链表

下一次调用New():优先再次取出这块内存,重复循环

3.2.2 重要细节如下:

1.obj->~T(); 手动调用析构函数

  1. 普通delete obj会做两件事:

    ① 调用析构函数 ② 调用 free 把内存还给系统堆

  2. 内存池不能执行②,内存我们要重复利用。

    所以我们

    手动只完成第一步:析构对象

举例子:如果T内部有动态开辟的缓冲区,析构函数里面会释放缓冲区,防止内存泄漏;

注意:只是清理对象内部资源,承载这个对象本身的那块内存,仍然保留!

2.*(void**)obj = _freeList;

重点难点:

obj原本是T*,指向一块存放 T 对象的内存。

现在对象已经析构完毕,这块内存暂时空闲,这块内存的数据已经没用了

于是我们直接复用这块内存最开头的 8 字节(64 位系统指针大小),用来保存链表指针。(其实这里也是关键,我们用这个可以自动识别当前程序在多少位系统下运行,32位的指针大小是4字节。Google牛批)

  • (void**)obj:把 T 对象的地址,强制转换成「指针的指针」
  • *(void**)obj:读写这块内存起始位置,写入旧链表头_freeList

效果:当前空闲块记住「下一个空闲块是谁」,形成单向链表。

3._freeList = obj;

更新链表头,把当前回收的内存块变成自由链表第一个节点

也就是头插法,O (1) 时间复杂度,不需要遍历链表,速度极快。

4.为什么敢直接覆盖 obj 头部内存?

因为已经执行 obj->~T(),对象生命周期结束,对象本身的数据不再需要,这块裸内存可以随便使用,用来存储链表指针完全安全。

5.为什么 New 拿到链表中的内存后,必须再次执行 new(obj) T

内存块只是一块空地。上次 Delete 的时候析构了对象,内存里是失效的数据。

必须依靠定位 new重新调用构造函数,初始化出新的合法对象。

6.和普通 delete 最大区别

复制代码
delete obj;               // 析构 + 归还内存给操作系统
TNPool.Delete(obj);       // 只析构,内存缓存复用,不归还系统

7.类型强转 *(void**)obj 的前提

我们在 New 的时候做了限制:

复制代码
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

保证每一块内存至少拥有存放一个指针的空间

防止 T 很小,内存空间不足以存放链表指针,导致越界。

3.3 TestObjectPool ()

3.3.1 函数目标

模拟高频、大量小对象反复创建销毁场景 ,对比「系统原生new/delete」与「自定义定长内存池」的执行耗时,验证理论:

频繁申请释放大量小对象时,内存池性能优于系统默认堆分配,同时减少内存碎片。

代码与注释如下:

复制代码
// 性能测试函数:对比 系统new/delete 和 自定义对象池 的速度差异
void TestObjectPool()
{
    const size_t Rounds = 3;    // 测试轮数:连续跑3轮,减少单次测试偶然性,结果更稳定
    const size_t N = 100000;    // 每一轮:申请、释放 10万个TreeNode节点

    // ========== 第一组实验:C++原生 new / delete ==========
    size_t begin1 = clock();    // clock()记录CPU开始滴答数,用来统计代码运行耗时
    std::vector<TreeNode*> v1; // 存放所有new出来的对象指针,后面统一释放
    v1.reserve(N);              // 提前预留vector存储空间
    // 重点:不加reserve,vector不断扩容、拷贝内存,会干扰计时结果!

    // 循环多轮,模拟持续不断的对象创建销毁压力
    for (size_t j = 0; j < Rounds; ++j)
    {
        // 批量申请10万个节点
        for (int i = 0; i < N; ++i)
        {
            v1.push_back(new TreeNode);
        }
        // 批量释放10万个节点
        for (int i = 0; i < N; ++i)
        {
            delete v1[i];
        }
        v1.clear(); // 清空容器,准备下一轮测试
    }
    size_t end1 = clock(); // 记录本组结束时间

    // ========== 第二组实验:我们实现的ObjectPool内存池 ==========
    ObjectPool<TreeNode> TNPool; // 创建TreeNode专用内存池
    size_t begin2 = clock();
    std::vector<TreeNode*> v2;
    v2.reserve(N); // 和上面保持一致,保证两组测试环境公平

    for (size_t j = 0; j < Rounds; ++j)
    {
        // 从内存池批量申请对象
        for (int i = 0; i < N; ++i)
        {
            v2.push_back(TNPool.New());
        }
        // 将对象归还内存池(不是释放给操作系统)
        for (int i = 0; i < N; ++i)
        {
            TNPool.Delete(v2[i]);
        }
        v2.clear();
    }
    size_t end2 = clock();

    // 打印两组消耗的时钟滴答数,数值越小运行越快
    cout << "new cost time:" << end1 - begin1 << endl;
    cout << "object pool cost time:" << end2 - begin2 << endl;
}
3.3.2为什么系统 new/delete 通常更慢?
  1. 通用堆分配器开销巨大

new底层调用malloc,需要维护整张堆空闲链表、合并空闲块、处理边界、全局锁;每次分配释放都有复杂算法。

  1. 频繁用户态 ↔ 内核态切换

大量小块内存反复申请,容易触发操作系统堆扩容,系统调用成本很高。

  1. 容易产生外部内存碎片

小块内存交替分配释放,堆上出现大量不连续小孔,后续分配越来越慢。

那我们的内存池又有怎样的优越性呢?老大?

哈哈哈哈哈,我这就解释:

话说回来,而内存池:

首次少量调用malloc申请大块内存;后续分配 / 回收只操作指针(用户态)、O (1) 链表头插 / 头取,没有复杂堆算法。

3.3.3 代码细节
  1. vector.reserve(N) 的意义

vector 默认空间不足时自动扩容,扩容会分配新内存 + 拷贝所有指针 + 释放旧内存

如果不提前reserve,扩容耗时会混入测试结果,破坏对比公平性。两组测试统一加上,消除容器带来的干扰。

  1. 先申请全部对象、再统一释放

模拟业务中:短时批量创建一堆对象,用完统一销毁的场景;

也可以改成「申请一个、立刻归还一个」的混合模式,两种负载都可以拓展测试。

  1. clock() 小知识点(拓展点)

clock()统计程序占用 CPU 的时间 ,不是墙上时钟;Windows 下CLOCKS_PER_SEC=1000,差值就是毫秒级滴答数。

缺点:精度一般;更高精度可以改用 C++11 std::chrono

3.3.4测试代码存在的缺陷

老大,你的代码好像存在不足。looking my eyes, why baby why?

what can you say?曼巴out

其实这是一个开胃小菜,既然你都讲出来了,那我就给你罗列出来吧:

  1. 测试顺序影响公平性

    先跑new/delete,再跑内存池。操作系统堆经过第一轮大量 new/delete 后产生碎片,可能让第一组天然变慢。

    ✅ 优化方案:多次运行、调换顺序(先测内存池,再测 new),取平均值。

  2. 当前内存池存在内存泄漏

    New()malloc出来的 128KB 大块内存,程序结束没有调用free

拓展优化:增加内存池析构函数,保存所有大块内存地址,退出统一释放。

  1. 只测试单线程场景

​ 原生malloc

​ 全局锁竞争在多线程下会急剧变慢;内存池可以改造为无 锁 / 分段锁,并发性能差距会更大。

3.4 定长内存池源码

ObjectPool.h

复制代码
#pragma once  // 头文件保护:防止这个头文件被重复包含,导致重复定义报错

// 引入需要的标准库
#include <iostream>   // 用于控制台打印输出
#include <vector>     // 用数组存申请的对象指针,方便批量释放
#include <ctime>      // 用于计时,对比两种分配方式的速度
#include <new>        // 提供「定位new」语法和内存分配失败的异常类型
using namespace std; 

// 定长内存池模板类:专门用来高效分配/释放 固定大小为T的对象
// 核心优势:比系统自带的new/delete快很多,还能减少内存碎片
template<class T>
class ObjectPool
{
public:
    // 功能:从内存池里申请一个T类型的对象,返回对象指针
    T* New()
    {
        T* obj = nullptr;  // 最终要返回的对象指针,先初始化为空

        // ---------------- 第一步:优先复用「别人用完还回来的」内存块 ----------------
        // _freeList 叫自由链表,串着所有回收回来的空闲内存块
        if (_freeList)
        {
            // 取出链表第一个节点里存的「下一个节点的地址」
            // 通俗理解:每个空闲块的前几个字节,我们都用来存下一个空闲块的地址,像链条一样串起来
            void* next = *(void**)_freeList;

            obj = (T*)_freeList;   // 把链表第一个块拿出来,给用户当对象用
            _freeList = next;      // 链表头往后挪一位,指向第二个节点
        }
        // ---------------- 第二步:没有回收块,就从大块内存里切一块 ----------------
        else
        {
            // 先检查:剩下的大块内存,够不够分一个对象出来
            if (_remainBytes < sizeof(T))
            {
                // 不够的话,就向系统一次性申请一大块内存(128KB = 128*1024字节)
                // 一次性申请大块,比一次次申请小块快很多,这是对象池提速的核心之一
                _memory = (char*)malloc(128 * 1024);
                if (_memory == nullptr)
                {
                    throw std::bad_alloc();  // 申请失败就抛异常
                }
                _remainBytes = 128 * 1024; // 更新剩余内存大小为刚申请的整块大小
            }

            obj = (T*)_memory;  // 从大块内存的开头,切出一块当对象

            // 计算实际分配大小:如果对象比指针还小,就按指针大小分配
            // 原因:回收的时候,每个块至少要能存下一个指针(用来串链表),不然放不下
            size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

            _memory += objSize;      // 大块内存指针往后挪,相当于切掉了刚分出去的那块
            _remainBytes -= objSize; // 剩余内存减去分出去的大小
        }

        // ---------------- 第三步:在内存上构造对象 ----------------
        // 定位new语法:在已经申请好的内存地址上,主动调用T的构造函数
        // 为什么要这一步?因为前面只拿到了裸内存,还没初始化对象,成员值都是乱的
        new(obj)T;

        return obj;
    }

    // 功能:把对象还回内存池,回收以后重复用
    void Delete(T* obj)
    {
        // 第一步:先主动调用析构函数,清理对象里的资源
        // 通俗理解:对象不用了,要先结束它的生命周期,比如对象里有malloc的内存,这里要释放
        obj->~T();

        // 第二步:用头插法,把这块内存插到自由链表的最前面
        // 头插最快,不用遍历链表,这也是对象池快的原因
        *(void**)obj = _freeList;  // 让当前块的前几个字节,存上原来的链表头地址
        _freeList = obj;           // 把链表头更新成当前块,它就成了第一个节点
    }

private:
    char* _memory = nullptr;    // 指向大块内存的起始地址,用char*是因为1字节好切分,像尺子一样量字节
    size_t _remainBytes = 0;    // 大块内存里,还剩多少字节没分配出去
    void* _freeList = nullptr;  // 自由链表的头指针,串着所有回收回来的空闲内存块
};


// 测试用的树节点结构体,用来当内存池的测试对象
struct TreeNode
{
    int _val;         // 节点存的值
    TreeNode* _left;  // 左孩子指针
    TreeNode* _right; // 右孩子指针

    // 构造函数:创建节点时,给成员赋初始值
    TreeNode()
        :_val(0)
        , _left(nullptr)
        , _right(nullptr)
    {
    }
};


// 性能测试函数:对比 系统new/delete 和 自定义对象池 的速度差异
void TestObjectPool()
{
    const size_t Rounds = 3;    // 测试轮数:跑3轮取平均,结果更准
    const size_t N = 100000;    // 每轮申请、释放的对象数量:10万个

    // ---------------- 第一组:用系统原生的 new / delete ----------------
    size_t begin1 = clock();    // 记录开始时间
    std::vector<TreeNode*> v1;  // 存所有申请的对象指针,方便批量释放
    v1.reserve(N);              // 提前预留空间,避免vector扩容影响计时

    // 循环Rounds轮,每轮申请N个、再释放N个
    for (size_t j = 0; j < Rounds; ++j)
    {
        // 批量申请
        for (int i = 0; i < N; ++i)
        {
            v1.push_back(new TreeNode);
        }
        // 批量释放
        for (int i = 0; i < N; ++i)
        {
            delete v1[i];
        }
        v1.clear(); // 清空数组,准备下一轮
    }
    size_t end1 = clock(); // 记录结束时间

    // ---------------- 第二组:用我们自己写的对象池 ----------------
    ObjectPool<TreeNode> TNPool; // 创建一个TreeNode类型的内存池
    size_t begin2 = clock();
    std::vector<TreeNode*> v2;
    v2.reserve(N);

    for (size_t j = 0; j < Rounds; ++j)
    {
        // 从对象池批量申请
        for (int i = 0; i < N; ++i)
        {
            v2.push_back(TNPool.New());
        }
        // 批量还回对象池
        for (int i = 0; i < N; ++i)
        {
            TNPool.Delete(v2[i]);
        }
        v2.clear();
    }
    size_t end2 = clock();

    // 打印两组耗时,数值越小速度越快
    cout << "new cost time:" << end1 - begin1 << endl;
    cout << "object pool cost time:" << end2 - begin2 << endl;
}

Test.cpp

复制代码
#include "ObjectPool.h"

int main()
{
	TestObjectPool();

	return 0;
}

又是美好的一天,各位观众姥爷们,若有什么不懂的地方,欢迎在评论区留言哦,我会给出相应解释的。如有什么疏忽的细节,请点出,我会及时纠正。感谢观看。喵喵

相关推荐
一只旭宝18 小时前
C++手写shared_ptr共享智能指针|原子引用计数、强弱引用控制块、赋值重载底层深度剖析
开发语言·c++·面试
开发者联盟league18 小时前
Java 通过 JNA 调用 C++ DLL:关键流程总结
java·c++·jna
库克克19 小时前
【C++】智能指针
java·开发语言·c++
我命由我1234519 小时前
Windows 操作系统 - 符号链接
linux·运维·windows·系统架构·操作系统·运维开发·系统
一拳一个呆瓜21 小时前
【STL】iostream 编程:输入流成员函数
c++·stl
xiaoye-duck1 天前
《Linux系统编程》Linux 系统多线程(七): C++ 线程安全日志系统封装:基于策略模式解耦,兼容 glog 使用风格
linux·c++·日志系统
程序喵大人1 天前
【C++进阶】STL容器与迭代器 - 07 迭代器是容器和算法之间的通用游标
开发语言·c++·算法
汉克老师1 天前
GESP2026年3月认证C++七级( 第三部分编程题(1、拆分))精讲
c++·预处理·dp·取模·gesp7级·数学规律·对数
BerryS3N1 天前
C++在AI时代的核心力量:从底层加速到大规模大模型推理引擎的高能实践
开发语言·c++·人工智能
FlightYe1 天前
音视频修炼之视频基础(一):视频基础理论
android·c++·vscode·音视频·androidx·android runtime