引言

在现代高性能软件开发中,内存管理往往是性能优化的关键战场。频繁的堆内存分配(new/delete)不仅会导致性能下降,还会引发内存碎片化问题,严重影响系统稳定性。本文将深入剖析高频调用模块中堆分配泛滥导致的性能塌方问题,并展示如何通过多种技术手段实现内存优化。

通过本文,读者将学习到:

  1. 如何诊断和分析内存碎片问题
  2. 内存池预分配技术的实现原理与应用
  3. 智能指针的性能优化技巧
  4. move语义的底层实现与零拷贝数据传输
  5. 自定义分配器(allocator)的设计方法

文章大纲

  1. 堆分配的性能代价与诊断
    • new/delete的隐藏成本
    • 内存碎片化问题分析
    • Valgrind工具链实战
  2. 内存池预分配技术
    • 内存池设计原理
    • 实现高性能对象池
    • 内存池的线程安全考量
  3. 智能指针优化策略
    • std::make_shared的优势分析
    • 控制块(control block)的内存布局
    • 引用计数的性能影响
  4. 零拷贝与move语义
    • move语义的汇编层解析
    • 完美转发(perfect forwarding)实现
    • 零拷贝数据传输案例
  5. 自定义分配器实战
    • 标准库兼容的allocator接口
    • 内存对齐(alignment)处理
    • 性能对比测试

1. 堆分配的性能代价与诊断

new/delete的隐藏成本

堆内存分配看似简单的操作,实际上包含多个隐藏步骤:

// 看似简单的new操作背后
void* operator new(size_t size) {void* p = malloc(size);       // 1. 向操作系统申请内存if (p == nullptr) {           // 2. 检查分配是否成功throw std::bad_alloc();   // 3. 失败时抛出异常}return p;                     // 4. 返回分配的内存
}

每次new操作平均需要100ns以上的时间,在高频调用场景下,这将成为性能瓶颈。更糟糕的是,频繁的分配释放会导致内存碎片化。

内存碎片化问题分析

内存碎片分为两种类型:

  • ​外部碎片​​:空闲内存分散在不连续的位置,无法满足大块内存请求
  • ​内部碎片​​:分配的内存块比实际需要的更大,导致浪费
65%35%内存碎片类型占比外部碎片内部碎片

Valgrind

Valgrind是一个基于动态二进制插桩(DBI)技术的开源内存调试工具,主要用于检测C/C++程序中的内存泄漏、非法访问、未初始化使用等内存问题。其核心工具Memcheck通过模拟CPU环境,在程序运行时插入检测代码,拦截所有内存操作(如malloc、free、new、delete等),并维护两个全局表——Valid-Address表(记录地址合法性)和Valid-Value表(跟踪值初始化状态)来验证每次内存访问的有效性。程序结束时,Valgrind会分析未释放的内存块及其分配调用栈,生成详细的泄漏报告(如"definitely lost"或"possibly lost"),同时能检测越界读写、重复释放等问题。尽管其运行时性能损耗较大(降低10-50倍速度),但无需修改源码即可实现深度检测,是开发阶段排查内存问题的利器。

Valgrind是强大的内存分析工具,可以检测内存泄漏和碎片问题:

valgrind --tool=memcheck --leak-check=full ./your_program

关键指标解读:

  • ​definitely lost​​:确认的内存泄漏
  • ​indirectly lost​​:间接泄漏(如数据结构中的泄漏)
  • ​possibly lost​​:可能的内存泄漏
  • ​still reachable​​:程序结束时仍可访问的内存

2. 内存池预分配技术

内存池设计原理

内存池(Memory Pool)是一种预先分配并管理固定大小内存块的高效内存管理技术。其核心原理是程序启动时一次性向系统申请一大块连续内存(称为"池"),将其分割为多个等长的内存块组成链表。当程序需要内存时,直接从池中分配现成的块,避免了频繁调用malloc/new的系统开销;释放时也不是真正返还系统,而是将块重新链入空闲链表供复用。这种设计显著减少了内存碎片,尤其适合频繁申请/释放小对象的场景(如网络连接、游戏对象),通过以空间换时间的策略,既提升了分配速度(O(1)O(1)O(1)时间复杂度),又保证了内存访问的局部性。典型的实现会维护空闲块指针,分配时移动指针并返回地址,释放时只需将内存块插回链表。

MemoryPool
+char* m_pool
+size_t m_size
+size_t m_used
+allocate(size_t size)
+deallocate(void* ptr) : void
+~MemoryPool()

实现高性能对象池

以下是线程安全对象池的实现示例:

template <typename T>
class ObjectPool {
public:ObjectPool(size_t chunkSize = 32) : m_chunkSize(chunkSize) {expandPool();}T* acquire() {std::lock_guard<std::mutex> lock(m_mutex);if (m_freeList.empty()) {expandPool();}T* obj = m_freeList.back();m_freeList.pop_back();return new (obj) T(); // placement new}void release(T* obj) {std::lock_guard<std::mutex> lock(m_mutex);obj->~T(); // 显式调用析构m_freeList.push_back(obj);}private:void expandPool() {size_t size = sizeof(T) * m_chunkSize;char* chunk = static_cast<char*>(::operator new(size));m_chunks.push_back(chunk);for (size_t i = 0; i < m_chunkSize; ++i) {m_freeList.push_back(reinterpret_cast<T*>(chunk + i * sizeof(T)));}}std::vector<char*> m_chunks;std::vector<T*> m_freeList;std::mutex m_mutex;size_t m_chunkSize;
};

内存池的线程安全考量

内存池的线程安全设计通常通过同步机制(如互斥锁、自旋锁或原子操作)来保证多线程环境下的正确分配和释放。核心原则是确保对空闲链表等共享数据结构的操作具有原子性:分配内存时需要加锁获取空闲块并移动指针,释放内存时同样加锁将块插回链表。细粒度锁(如每个内存块或子池独立加锁)可提升并发性能,但会增加实现复杂度;无锁设计(如CAS原子操作管理链表指针)能彻底避免线程阻塞,但对算法要求较高。此外还需注意"伪共享"问题(频繁操作的指针避免位于同一缓存行),以及线程局部缓存(Thread-Local Storage)的运用——每个线程维护独立的小内存池,仅当不足时才访问全局池,可大幅减少锁竞争。

多线程环境下,内存池需要考虑:

  1. ​锁粒度​​:细粒度锁 vs 全局锁
  2. ​线程局部存储​​(TLS):减少锁争用
  3. ​无锁设计​​:原子操作实现
线程请求内存
线程局部内存池有空间?
从TLS分配
获取全局锁
从全局池分配大块
分割到TLS

3. 智能指针优化策略

std::make_shared的优势分析

std::make_shared 相比直接使用 std::shared_ptr 构造函数主要有两大优势:​​内存效率​​和​​异常安全​​。首先,make_shared 会一次性分配内存,既存储对象本身,又存储控制块(引用计数等),而直接构造 shared_ptr 则需要两次独立分配(对象和控制块),减少了内存碎片和开销。其次,make_shared 是异常安全的,如果对象构造过程中抛出异常,不会留下悬空的裸指针,而直接构造 shared_ptr 时若 new 成功但 shared_ptr 构造失败,则会导致内存泄漏。此外,make_shared 语法更简洁,避免了显式 new 操作,符合现代 C++ 的 RAII 原则。

// 传统方式:两次堆分配
std::shared_ptr<Widget> sp1(new Widget);// 优化方式:单次堆分配
auto sp2 = std::make_shared<Widget>();

内存布局对比:

new Widget
Widget对象
new ControlBlock
引用计数等
make_shared
连续内存块
Widget对象
ControlBlock

控制块的内存布局

std::shared_ptr 的控制块是一个动态分配的内存结构,通常包含两个引用计数器strong_refsweak_refs)、指向被管理对象的指针ptr)、以及可选的删除器deleter)和分配器allocator)。强引用计数strong_refs)管理对象的生命周期,当减至零时调用析构函数;弱引用计数weak_refs)仅控制控制块本身的生命周期,当强弱引用均归零时才释放控制块。控制块通常位于对象内存附近(若使用 std::make_shared 则可能与对象连续存储),但独立于 shared_ptr 实例本身,所有共享同一对象的 shared_ptr 副本都通过原子操作修改同一控制块,确保线程安全。这种设计使得引用计数的增减和对象析构具有原子性,但也带来了循环引用的风险(需配合 std::weak_ptr 解决)。

std::shared_ptr的控制块包含:

  • 强引用计数
  • 弱引用计数
  • 删除器(deleter)
  • 分配器(allocator)
  • 指向对象的指针

引用计数的性能影响

引用计数(Reference Counting)虽然简化了内存管理,但会带来显著的性能开销:每次拷贝、赋值或销毁智能指针时都需要执行​​原子操作​​修改引用计数,这会导致​​缓存一致性同步​​(CPU核心间频繁同步缓存行),在高并发场景下可能引发​​竞争瓶颈​​。此外,循环引用会导致对象无法释放(内存泄漏),而弱引用(weak_ptr)的引入又增加了额外的控制块访问开销。对于频繁传递的小对象,引用计数的开销可能超过对象本身的操作成本,此时更适合使用移动语义(如unique_ptr)或栈分配。优化手段包括局部性优化(如make_shared合并内存分配)、减少不必要的拷贝,或在确定性场景中改用作用域指针(如RAII管理)。

引用计数操作需要原子操作,在多核CPU上可能引发缓存一致性问题:

; x86汇编示例
lock inc dword [rcx]  ; 原子递增操作

优化策略:

  1. 减少std::shared_ptr的拷贝
  2. 使用std::move转移所有权
  3. 考虑std::weak_ptr打破循环引用

4. 零拷贝与move语义

move语义的汇编层解析

move语义的本质是资源所有权的转移,而非数据的物理移动。

Move语义在汇编层面的本质是​​避免不必要的内存拷贝​​,通过将源对象的资源指针/句柄直接转移给目标对象实现高效传递。

std::string为例:传统拷贝构造在汇编中会调用memcpy复制堆内存(生成mov指令序列),而move构造仅传递内部指针(如mov rax, [src]将堆地址存入目标对象,并置空源对象指针如mov [src], 0)。

关键区别在于move操作不触发资源实际复制,仅重组指针所有权,其汇编代码通常仅包含寄存器操作(如xchg)和指针清零,无堆内存访问(如call malloc)。

编译器对右值引用(T&&)的优化会消除临时对象,最终生成的汇编指令数可能比拷贝少一个数量级,尤其在传递容器(如std::vector)时,move仅交换3个指针(首/尾/容量),而拷贝需遍历所有元素。

以下代码展示move前后的变化:

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);

对应的汇编伪代码:

; v1的原始状态
mov rdi, [v1._M_start]
mov rsi, [v1._M_finish]
mov rdx, [v1._M_end_of_storage]; move操作后
mov [v2._M_start], rdi
mov [v2._M_finish], rsi
mov [v2._M_end_of_storage], rdx
xor edi, edi
mov [v1._M_start], rdi
mov [v1._M_finish], rdi
mov [v1._M_end_of_storage], rdi

完美转发实现

完美转发(Perfect Forwarding)是 C++11 引入的核心技术,通过​​右值引用​​(T&&)和 std::forward 实现函数模板将参数​​原样转发​​给其他函数,保留其值类别(左值/右值)和 const 属性。

其本质是引用折叠规则T& &T&T&& &&T&&)与模板类型推导的配合:当模板参数 T 接收左值时推导为 T&,接收右值时推导为 T&&std::forward 则根据 T 的实际类型决定转发为左值(static_cast<T&>)或右值(static_cast<T&&>)。

template <typename T>
void wrapper(T&& arg) {target(std::forward<T>(arg));
}

模板类型 T 推导:

  • 左值参数:T推导为T&,T&&为T&(引用折叠
  • 右值参数:T推导为T,T&&为T&&

典型应用场景是工厂函数或包装器(如 emplace_back),确保参数在多层传递中保持原始语义,避免不必要的拷贝或丢失移动机会。例如 logAndCreate(T&& arg)arg 完美转发给构造函数时,若原始参数是右值则触发移动语义,左值则保持拷贝,实现零开销抽象。

零拷贝数据传输案例

网络编程中的零拷贝示例:

// 传统方式:多次拷贝
void sendPacket(const std::string& data) {char* buffer = new char[data.size()];std::copy(data.begin(), data.end(), buffer);socket.write(buffer, data.size());delete[] buffer;
}// 零拷贝方式
void sendPacket(std::string&& data) {socket.write(data.data(), data.size());// 无需拷贝,直接使用内部缓冲区
}

这段代码展示了​​零拷贝优化​​的核心思想:通过移动语义避免不必要的数据复制。传统方式中,sendPacket 接收 const std::string& 时无法修改源数据,必须分配新缓冲区并逐字节拷贝(std::copy),导致两次内存操作(堆分配+复制)。而零拷贝版本接收右值引用(std::string&&),直接访问源字符串的内部缓冲区(data.data()),由于调用者已声明放弃所有权(如传递临时对象或显式 std::move),函数可以安全"窃取"其内存资源而不破坏语义。这不仅省去了堆分配和复制的开销(从 O(n) 降至 O(1)),还保持了原始数据的连续性,尤其对大容量数据(如网络包)性能提升显著。关键点在于移动后的字符串处于有效但未定义状态,适合立即销毁或重新赋值的场景。

5. 自定义分配器实战

标准库兼容的allocator接口

标准库兼容的分配器(Allocator)接口是一组用于内存管理的​​泛型契约​​,要求实现 allocatedeallocate 等核心方法,并满足 rebind 模板机制以适配不同类型。其核心规范包括:1) 类型定义(如 value_typepointer);2) 内存操作allocate(n) 分配未构造内存,deallocate(p, n) 释放时需大小匹配);3) 构造/析构工具construct(p, args)destroy(p),C++20 后通常省略);4) 传播特性(通过 propagate_on_container_* 类型控制容器拷贝时的分配器行为)。

标准分配器需保证线程安全,且允许自定义实现(如内存池或共享内存分配器),只要满足接口约束即可无缝替换 std::allocator,使容器(如 vector)自动采用定制策略。关键是通过统一接口解耦内存分配与对象生命周期管理,支持从默认 new/delete 到复杂内存模型的灵活扩展。

符合C++标准的allocator需要实现以下关键接口:

template <typename T>
class CustomAllocator {
public:using value_type = T;CustomAllocator() noexcept = default;template <typename U>CustomAllocator(const CustomAllocator<U>&) noexcept {}T* allocate(size_t n) {return static_cast<T*>(::operator new(n * sizeof(T)));}void deallocate(T* p, size_t) {::operator delete(p);}template <typename U>bool operator==(const CustomAllocator<U>&) { return true; }template <typename U>bool operator!=(const CustomAllocator<U>&) { return false; }
};

内存对齐处理

内存对齐(Memory Alignment)是指数据在内存中的存储地址按照特定字节边界(如4、8、16字节)排列,以匹配CPU访问内存的最优粒度

现代处理器通常要求特定类型的数据(如double或SSE指令操作数)必须对齐到其大小的整数倍地址,否则可能引发性能下降(如x86上的非对齐访问惩罚)或直接错误(如ARM的硬件异常)。

编译器默认通过插入填充字节(Padding)实现结构体成员对齐(如struct { char c; int i; }会在c后填充3字节),也可用alignas关键字显式指定对齐方式(如alignas(16) float arr[4])。

对齐处理的关键在于平衡内存利用率与CPU访问效率,高性能场景(如SIMD或缓存行优化)常需手动调整对齐策略,而C++11引入的alignofstd::aligned_storage等工具则提供了跨平台的对齐控制能力。

template <size_t Alignment>
class AlignedAllocator {static_assert(Alignment > 0, "Alignment must be positive");void* allocate(size_t size) {return aligned_alloc(Alignment, size);}void deallocate(void* p) {free(p);}
};

性能对比

系统 malloc 作为通用内存分配器,依赖操作系统管理,适合通用场景但性能较低(频繁系统调用、锁竞争和内存碎片)。

​内存池​​通过预分配和复用内存块,减少系统调用和碎片,提升分配速度,但仍有全局锁开销。

​无锁内存池​​基于原子操作(如CAS)实现并发安全,兼顾多线程性能与内存利用率,但实现复杂且需处理ABA问题。

​TLS内存池​​(线程本地存储)为每个线程维护独立内存池,彻底消除锁竞争,适合高频分配场景,但可能造成线程间内存利用率不均。

综合来看,性能排序通常为:TLS内存池 > 无锁内存池 > 普通内存池 > 系统 malloc,但选择需权衡场景特性(如线程数、分配频率和实时性要求)。

结论

通过内存池预分配、智能指针优化和move语义的应用,我们可以显著减少高频调用场景下的内存分配开销。关键优化点包括:

  1. 使用内存池减少系统调用和碎片化
  2. 优先选择std::make_shared创建智能指针
  3. 利用move语义实现零拷贝数据传输
  4. 为特定场景设计自定义分配器

实际项目中,建议结合性能分析工具(如perf、VTune)进行量化评估,确保优化措施确实带来预期收益。

参考资料

  1. C++标准库allocator要求
  2. Intel TBB内存分配器
  3. C++ Core Guidelines: 资源管理

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【GoLang#2】:基础入门(工具链 | 基础语法 | 内置函数)

前言&#xff1a;Go 的一些必备知识 1. Go 语言命名 Go的函数、变量、常量、自定义类型、包(package)的命名方式遵循以下规则&#xff1a; 首字符可以是任意的Unicode字符或者下划线剩余字符可以是Unicode字符、下划线、数字字符长度不限 Go 语言代码风格及开发事项代码每一行结…

Bert项目--新闻标题文本分类

目录 技术细节 1、下载模型 2、config文件 3、BERT 文本分类数据预处理流程 4、对输入文本进行分类 5、计算模型的分类性能指标 6、模型训练 7、基于BERT的文本分类预测接口 问题总结 技术细节 1、下载模型 文件名称--a0_download_model.py 使用 ModelScope 库从模型仓…

sendfile系统调用及示例

好的&#xff0c;我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 sendfile 函数&#xff0c;它是一个高效的系统调用&#xff0c;用于在两个文件描述符之间直接传输数据&#xff0c;通常用于将文件内容发送到网络套接字&#xff0c;而无需将数据从内核空间复制到用户空…

数据结构习题--删除排序数组中的重复项

数据结构习题–删除排序数组中的重复项 给你一个 非严格递增排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 方法&…

Docker的容器设置随Docker的启动而启动

原因也比较简单&#xff0c;在docker run 的时候没有设置–restartalways参数。 容器启动时&#xff0c;需要增加参数 –restartalways no - 容器退出时&#xff0c;不重启容器&#xff1b; on-failure - 只有在非0状态退出时才从新启动容器&#xff1b; always - 无论退出状态…

JWT安全机制与最佳实践详解

JWT&#xff08;JSON Web Token&#xff09; 是一种开放标准&#xff08;RFC 7519&#xff09;&#xff0c;用于在各方之间安全地传输信息作为紧凑且自包含的 JSON 对象。它被广泛用于身份验证&#xff08;Authentication&#xff09;和授权&#xff08;Authorization&#xff…

如何解决pip安装报错ModuleNotFoundError: No module named ‘ipython’问题

【Python系列Bug修复PyCharm控制台pip install报错】如何解决pip安装报错ModuleNotFoundError: No module named ‘ipython’问题 摘要 在开发过程中&#xff0c;我们常常会遇到pip install报错的问题&#xff0c;其中一个常见的报错是 ModuleNotFoundError: No module named…

从三维Coulomb势到二维对数势的下降法推导

题目 问题 7. 应用 9.1.4 小节描述的下降法&#xff0c;但针对二维的拉普拉斯方程&#xff0c;并从三维的 Coulomb 势出发 KaTeX parse error: Invalid delimiter: {"type":"ordgroup","mode":"math","loc":{"lexer&qu…

直播一体机技术方案解析:基于RK3588S的硬件架构特性​

硬件配置​​主控平台​​▸ 搭载瑞芯微RK3588S旗舰处理器&#xff08;四核A762.4GHz 四核A55&#xff09;▸ 集成ARM Mali-G610 MP4 GPU 6TOPS算力NPU▸ 双通道LPDDR5内存 UFS3.1存储组合​​专用加速单元​​→ 板载视频采集模块&#xff1a;支持4K60fps HDMI环出采集→ 集…

【氮化镓】GaN取代GaAs作为空间激光无线能量传输光伏转换器材料

2025年7月1日,西班牙圣地亚哥-德孔波斯特拉大学的Javier F. Lozano等人在《Optics and Laser Technology》期刊发表了题为《Gallium nitride: a strong candidate to replace GaAs as base material for optical photovoltaic converters in space exploration》的文章,基于T…

直播美颜SDK动态贴纸模块开发指南:从人脸关键点识别到3D贴合

很多美颜技术开发者好奇&#xff0c;如何在直播美颜SDK中实现一个高质量的动态贴纸模块&#xff1f;这不是简单地“贴图贴脸”&#xff0c;而是一个融合人脸关键点识别、实时渲染、贴纸驱动逻辑、3D骨骼动画与跨平台性能优化的系统工程。今天&#xff0c;就让我们从底层技术出发…

学习游戏制作记录(剑投掷技能)7.26

1.实现瞄准状态和接剑状态准备好瞄准动画&#xff0c;投掷动画和接剑动画&#xff0c;并设置参数AimSword和CatchSword投掷动画在瞄准动画后&#xff0c;瞄准结束后才能投掷创建PlayerAimSwordState脚本和PlayerCatchSwordState脚本并在Player中初始化&#xff1a;PlayerAimSwo…

【c++】问答系统代码改进解析:新增日志系统提升可维护性——关于我用AI编写了一个聊天机器人……(14)

在软件开发中&#xff0c;代码的迭代优化往往从提升可维护性、可追踪性入手。本文将详细解析新增的日志系统改进&#xff0c;以及这些改进如何提升系统的实用性和可调试性。一、代码整体背景代码实现了一个基于 TF-IDF 算法的问答系统&#xff0c;核心功能包括&#xff1a;加载…

visual studio2022编译unreal engine5.4.4源码

UE5系列文章目录 文章目录 UE5系列文章目录 前言 一、ue5官网 二.编译源码中遇到的问题 前言 一、ue5官网 UE5官网 UE5源码下载地址 这样虽然下载比较快,但是不能进行代码git管理,以后如何虚幻官方有大的版本变动需要重新下载源码,所以我们还是最好需要visual studio2022…

vulhub Earth靶场攻略

靶场下载 下载链接&#xff1a;https://download.vulnhub.com/theplanets/Earth.ova 靶场使用 将压缩包解压到一个文件夹中&#xff0c;右键&#xff0c;用虚拟机打开&#xff0c;就创建成功了&#xff0c;然后启动虚拟机&#xff1a; 这时候靶场已经启动了&#xff0c;咱们现…

Python训练Day24

浙大疏锦行 元组可迭代对象os模块

Spring核心:Bean生命周期、外部化配置与组件扫描深度解析

Bean生命周期 说明 程序中的每个对象都有生命周期&#xff0c;对象的创建、初始化、应用、销毁的整个过程称之为对象的生命周期&#xff1b; 在对象创建以后需要初始化&#xff0c;应用完成以后需要销毁时执行的一些方法&#xff0c;可以称之为是生命周期方法&#xff1b; 在sp…

日语学习-日语知识点小记-进阶-JLPT-真题训练-N1阶段(1):2017年12月-JLPT-N1

日语学习-日语知识点小记-进阶-JLPT-真题训练-N1阶段&#xff08;1&#xff09;&#xff1a;2017年12月-JLPT-N1 1、前言&#xff08;1&#xff09;情况说明&#xff08;2&#xff09;工程师的信仰&#xff08;3&#xff09;真题训练2、真题-2017年12月-JLPT-N1&#xff08;1&a…

(一)使用 LangChain 从零开始构建 RAG 系统|RAG From Scratch

RAG 的主要动机 大模型训练的时候虽然使用了庞大的世界数据&#xff0c;但是并没有涵盖用户关心的所有数据&#xff0c; 其预训练令牌&#xff08;token&#xff09;数量虽大但相对这些数据仍有限。另外大模型输入的上下文窗口越来越大&#xff0c;从几千个token到几万个token,…

OpenCV学习探秘之一 :了解opencv技术及架构解析、数据结构与内存管理​等基础

​一、OpenCV概述与技术演进​ 1.1技术历史​ OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是由Intel于1999年发起创建的开源计算机视觉库&#xff0c;后来交由OpenCV开源社区维护&#xff0c;旨在为计算机视觉应用提供通用基础设施。经历20余年发展&…