高性能对象池实现

导语 | 内存池用于对频繁申请的内存进行管理进而提升分配效率,但缺乏对一些创建和销毁开销比较大的对象的复用手段,因此对象池应运而生。而当系统中存在大量对象需要频繁创建和销毁时,如何减少大量的耗时开销是对象池构建的关键点之一,本文以此出发,与大家共同探讨高性能对象池的实现。文章作者:杨哲,腾讯WXG后台研发工程师。


一、背景

内存池用于对频繁申请的内存进行管理,通过合理的分配策略和内存布局来减少内存的碎片化以及提高内存的分配效率。但是对于一些创建和销毁开销大的对象,内存池缺乏对这些对象进行复用的手段,因此出现了对象池。

从内存分配的角度来看,相对于内存池,对象池管理的是定长内存,所以无需考虑内存碎片的问题,在内存管理策略上也更加的简单。我们的系统中存在的大量对象需要频繁地创建和销毁,产生了大量的耗时开销,因此需要对象池提供对象复用的方式来避免构造析构产生的开销,或者是通过对象的重置来减少创建销毁对象的开销。

另一方面相对于目前的内存分配器,对象内存的管理更加简单,因此相对于现有的内存分配器在内存分配和释放的效率有一定的提升空间。


二、目标

  • 对象可复用:通过复用对象来避免频繁地调用 malloc 和 free 函数,或者是减少构造析构产生的开销,从而提升性能;

  • 高性能:高性能是设计这个对象池最主要的目标,这里的高性能指的是内存分配和释放的开销足够低;

  • 线程安全:对象池可能会被多个线程同时访问,因此要保证对象池的线程安全;

  • 对象的容量支持动态扩展;

  • 优先分配使用过的对象。


三、方案调研

1. 对象池

(1)brpc object pool


brpc object pool 通过批量分配和归还内存以避免全局竞争,从而降低单次分配、释放的开销。brpc object pool 每个线程的分配流程如下:

  1. 查看 thread-local free block 或者空闲对象数组。如果还有 free 的对象,返回。没有的话步骤2。

  2. 尝试从全局资源池取一块空闲的空间,若取到的话回到步骤1,否则步骤3。

  3. 从全局资源池从系统申请一大块内存,返回其中第一个对象。

释放流程为将对象回收到 thread-local 的空闲对象数组中,攒够数量后回收到全局资源池。

(2)go对象池

Pool 会为每个协程维护一个本地池,本地池分为私有池 private 和共享池 shared。私有池中的元素只能本地协程使用,共享池中的元素可能会被其他协程偷走,所以使用私有池 private 时不用加锁,而使用共享池 shared 时需加锁。


通过对象池获取对象时会优先查找本地 private 池,再查找本地 shared 池,最后查找其他协程的 shared 池,如果以上全部没有可用元素,最后会调用 New 函数获取新元素。详细的分配过程如下图所示:

回收对象时优先把元素放在 private 池中。如果 private 不为空,则放在 shared 池中。

(3)Netty recycler

每个线程都拥有 thread-local 的 Stack, 在 Stack 中维护对象数组以及 WeakOrderQueue 的相关指针。对于全局资源分配机制,当本线程 thread1 回收本线程产生的对象时, 会将对象以 DefaultHandle 的形式存放在 Stack 中。其它线程  thread2 也可以回收 thread1 产生的对象,thread2 回收的对象不会立即放回  thread1 的 Stack 中,而是保存在 thread2 内部的一个 WeakOrderQueue 中。这些外部线程的 WeakOrderQueue 以链表的方式和 Stack 关联起来。

默认情况下一个线程最多持有 2*核数个 WeakOrderQueue,也就是说一个线程最多可以帮 2*核数个外部线程的对象池回收对象。WeakOrderQueue 内部有以 Link 来管理对象。每个 Link 存放的对象是有限的,一个 Link 最多存放16个对象。如果满了则会再产生一个Link 继续存放。

当前线程从对象池中拿对象时, 首先从 Stack中获取,若没有的话,将尝试从 cursor 指向的 WeakOrderQueue 中回收一个 Link 的对象,。如果回收到了就继续从Stack中获取对象;如果没有回收到就创建对象。一个对象池中最多存放 4K 个对象 , Link节点中每个 DefaultHandle 数组默认长度 16,这两个参数可以控制。

2. 内存池

虽然内存池使用的场景和对象池有区别,除了分配的速度外内存池还需要考虑内存碎片的问题,但是内存池在应对多线程访问时的减少锁竞争思路是可以借鉴的。


(1)tcmalloc

在 tcmalloc 中每个线程都有一个线程局部的 ThreadCache,按照对象的大小进行分类维护对象的链表。如果 ThreadCache 的对象不够了,就从 CentralCache 进行批量分配。如果 CentralCache 没有可分配的对象,就从 PageHeap 申请 Span。如果  PageHeap 没有合适的 Page,就从操作系统申请。


在释放内存的时候,ThreadCache 依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache。CentralCache 发现一个 Span 的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap 发现一批连续的 Page 都释放了,就可以归还给操作系统。

(2)jemalloc

虚拟内存被逻辑上分割成 chunks(默认是4MB,1024个4k页),访问线程通过 round-robin 算法在第一次 malloc 的时候分配 arena,每个 arena 都是相互独立的,维护自己的 chunks, chunk 切割 pages 到 small/large 对象。free() 的内存总是返回到所属的 arena 中,而不管是哪个线程调用 free()。通过 arena 分配的时候需要对 arena bin(每个 small size-class 一个,细粒度)加锁,或 arena 本身加锁。并且线程 cache 对象也会通过垃圾回收指数退让算法返回到 arena 中。


四、整体设计

从上面的对内存分配系统的调研来看,在应对多线程访问时为了减少锁竞争的方式大体上一致,都是通过分区减小锁的粒度以及使用 TLS 来实现每个线程独享的资源池来避免大部分的锁竞争。所以本文中对象池在保存空闲对象时使用 freelist + TLS + 多资源池的组合,使用 freelist 可以节省指针部分的内存,而且在交换资源时只需对队头指针进行修改,速度非常快而且减少了在临界区中的耗时,缓解了公共资源池中的锁竞争。对象池的整体结构图如下:

一个 Object Pool 中主要有两个部分:

1. Local Pool

每个访问对象池的线程都会独自拥有一个 Local Pool,使用 TLS(Thread-Local Storage) 实现,Object Pool 中使用了一个 thread_local 指针指向一个 Local Pool,访问对象池的线程与 Local Pool 直接交互,申请的对象直接从 Local Pool 中获取,释放的对象也直接归还到 Local Pool 中。

Local Pool 维护一个 Block 指针和空闲队列(FreeSlots),其中 Block 只负责分配对象,对象只会回收到空闲队列中而不会回收到 Block 中,Local Pool 中的空闲队列达到一定长度就会回收到 Global Pool 中。当 Local Pool 对象不足时就会从 Global Pool 中申请对象资源,Global Pool 会把空闲的链表或者 Block 给 Local Pool。

2. Global Pool

负责整体的内存资源申请。Global Pool 中维护了 BlockManager 和 FreeslotsManager 两个数据结构,其中 BlockManager 用于管理 BlockChunk,每个 BlockChunk 中包含多个 Block,如果当前 BlockChunk 耗尽,那么会 Global Pool 会 new 一个新的 BlockChunk。

FreeSlotsManager 用于管理 Global Pool 中的空闲链表,如果 FreeSlots 中的空闲链表都回收自 Local Pool,当 Local Pool 中的空闲链表的长度到达 kFreeslotsSize 时,就会将该空闲链表回收到 Global Pool 的 FreeSlotsManager 中,所以 FreeSlotsManager 中每条空闲链表的长度都是 kFreeSlotsSize。

Global Pool 的数量可以是多个,这个参数是可以设置的,设置多个 Global Pool 可以缓解 Global Pool 的锁竞争问题从而减少耗时,但可能会带来一定的内存膨胀,可以根据访问线程个数等因素来通过合理设置 Global Pool 的数量在速度与内存之间进行平衡。


这种将对象池分离成 Local Pool 和 Global Pool 的设计有利于避免激烈的锁竞争,只有涉及到 Global Pool 与 Local Pool 的资源交换时才会出现锁竞争,大部分情况下线程只和 Local Pool 进行交互就可以完成资源的申请、释放,所以大大地提高了性能。


五、实现

下面是对象池中的数据结构:(设对象池中需要分配的对象为T)

1. Slot

union Slot {  Slot *next_ = NULL;  T val_;};

Slot都是一个对象的内存单位,使用 union 的原因是:Slot 有两种状态,一种是作为分配出去的对象使用,另一种是未分配时作为空闲链表中的一个结点。

2. Block

struct Block {  Slot slots_[kBlockSize];  size_t idx_ = 0;};

Local Pool 向 Global Pool 申请资源的基本单位,每个 Local Pool 中会维护一个 Block,当空闲链表用光时,对象内存从 Block 中获取。

3. BlockChunk

struct BlockChunk {  Block blocks_[kBlockChunkSize];  size_t idx_ = 0;};

Global Pool 申请资源的基本单位,每个 BlockChunk 中包含了 kBlockChunkSize 个 Block。

4. FreeSlots

struct FreeSlots {  Slot *head_ = NULL;  size_t length_ = 0;};

空闲链表,这里维护了空闲链表的队头指针,以及目前链表的长度。

5. BlockManager

struct BlockManager {  std::vector<BlockChunk*> block_chunks_;};

Global Pool 中管理 BlockChunk 的数据结构,便于最后释放整个对象池的资源。

6. FreeSlotsManager

struct FreeSlotsManager {  size_t free_num_ = 0;  std::vector<Slot*> freeslots_ptrs;};

Global Pool 中管理空闲链表的数据结构,freeslots_ptrs 中的每个非空指针对应一条长度为 kFreeSlotsSize 的空闲链表。

7. Local Pool


LocalPool 的数据结构定义如下:主要有3个成员变量,global_pool_ 用于在要和 Global Pool 进行资源交换时调用 Global Pool 的对应接口,block_ 用于维护一块可用的 Block,作为当 Local Pool 和 Global Pool 中所有的空闲链表都消耗完时的备用内存,freeslots_ 为 Local Pool 中维护的空闲链表。

class GlobalPool {    GlobalPool<T> *global_pool_;    Block<T> *block_;    FreeSlots<T> freeslots_;}

(1)分配对象流程

  • 如果 Local Pool 的 freeslots 中存在空闲对象,直接从 freeslots 中分配一个 Slot 出去;

  • 如果 Local Pool 的 freeslots 分配完了,那么从 Global Pool 的 FreeSlotsManager 中查看是否有可用的空闲链表,如果有那么分配一条空闲链表到 Local Pool 的 freeslots 中,然后从 freeslots 中分配一个 Slot 出去;

  • 如果 Global Pool 的 FreeSlotsManager 中的空闲链表分配完了,那么从 Local Pool 的 block_ 分配一个 Slot 出去;

  • 如果 Local Pool 中的 block_ 用光了,从 Global Pool 中申请一个 Block,然后从这个 Block 中分配一个 Slot 出去;

  • 如果 Global Pool 中的 Block 用光了,new 一个新的 BlockChunk,然后将其中一个 Block 分配给 Local Pool。

T* GetObject() {        // 如果freeslots还有可用空间        if (freeslots_.head_ != NULL) {            Slot<T> *res = freeslots_.head_;            freeslots_.head_ = res->next_;            freeslots_.length_--;            return (T*)res;        }        // 如果global pool中有可用的freeslots        else if (global_pool_->PopFreeSlots(freeslots_)) {            Slot<T> *res = freeslots_.head_;            freeslots_.head_ = res->next_;            freeslots_.length_--;            return (T*)res;        }        // 如果local pool的block还有可用空间        else if (block_->idx_ < kBlockSize) {            return (T*)&block_->slots_[block_->idx_++];        }        // 如果global pool还有可用的block        else if (block_ = global_pool_->PopBlock()) {            return (T*)&block_->slots_[block_->idx_++];        }

        // 没有可用的空间        return NULL;    }

(2)回收对象流程


所有回收的内存都是回收到 Local Pool 的 freeslots 中,回收的内存插入到空闲链表的头部,如果插入后 freeslots 的长度达到 kFreeSlotsSize,那么将这条 Local Pool 中的空闲链表回收到 Global Pool 中。

void ReturnObject(T *obj) {        // 如果freeslots还剩最后一个slot的回收空间        ((Slot<T>*)obj)->next_ = freeslots_.head_;        freeslots_.head_ = (Slot<T>*)obj;        freeslots_.length_++;

        // 如果freeslots_中的长度满足条件,回收到global pool中        if (freeslots_.length_ == kFreeSlotsSize) {            global_pool_->PushFreeSlots(freeslots_);        }}

8. Global Pool

GlobalPool 的数据结构定义如下:主要有4个成员变量,block_manager_ 为 BlockChunk 管理单元,freeslots_manager_ 为空闲链表,其中可以维护多条空闲链表。由于在 Global Pool 中会存在多个线程同时进行资源交换,因此需要对 block_manager_ 和 freeslots_manager_ 进行操作时需要上锁,为了减少锁的粒度分别对两个资源管理单元使用不同的锁,其中 freeslots_lck_ 对应 freeslots_manager_,block_mtx_ 对应 block_manager。

class GlobalPool {    BlockManager<T> block_manager_;    FreeSlotsManager<T> freeslots_manager_;

    pthread_spinlock_t freeslots_lck_;    pthread_mutex_t block_mtx_;}

(1)Global Pool 中的资源操作

  • 从 FreeSlotsManager 中取出空闲链表,如果 freeslots_manager_ 存在可用的空闲链表,取出空闲链表给 Local Pool。

bool PopFreeSlots(FreeSlots<T> &freeslots) {
        pthread_spin_lock(&freeslots_lck_);
        // 如果Global Pool中有可用的空闲链表
        if (freeslots_manager_.free_num_ > 0) {
            freeslots.head_ = freeslots_manager_.freeslots_ptrs[--freeslots_manager_.free_num_];
            pthread_spin_unlock(&freeslots_lck_);
            // Global Pool中每条空闲链表的长度都为kFreeSlotsSize
            freeslots.length_ = kFreeSlotsSize;
            return true;
        }
        pthread_spin_unlock(&freeslots_lck_);
        return false;
    }
  • 将 LocalPool 中的空闲链表回收到 Global Pool。

bool PushFreeSlots(FreeSlots<T> &freeslots) {
        pthread_spin_lock(&freeslots_lck_);
        
        // 如果freeslots_manager中存储的空闲链表的指针位置不够用,增加1000个位置
        if (freeslots_manager_.free_num_ >= freeslots_manager_.freeslots_ptrs.size()) {
            freeslots_manager_.freeslots_ptrs.resize(freeslots_manager_.freeslots_ptrs.size() + 1000);
        }


  // 将Local Pool的空闲链表的队头指针存储到freeslots_manager中
        freeslots_manager_.freeslots_ptrs[freeslots_manager_.free_num_++] = freeslots.head_;
        pthread_spin_unlock(freeslots_lck_);
        // 重置Local Pool中空闲链表的信息
        freeslots.head_ = NULL;
        freeslots.length_ = 0;
        return true;
    }
  • 分配 Block 给 Local Pool,如果 BlockManager 中当前 BlockChunk 存在空闲的 Block,那么直接分配给 Local Pool;如果 BlockManager 中的 Block 已经用完那么申请一个新的 BlockChunk,并从 BlockChunk 中划分出 Block 给 Local Pool。

// 申请空间
bool NewBlockChunk() {
    BlockChunk<T> *new_block_chunk = new (std::nothrow) BlockChunk<T>;
    if (unlikely(new_block_chunk == NULL))
        return false;


    block_manager_.block_chunks_.push_back(new_block_chunk);
    return true;
}


Block<T>* PopBlock() {
    pthread_mutex_lock(&block_mtx_);
    BlockChunk<T>* block_chunk = block_manager_.block_chunks_.back();
    // 如果当前BlockChunk已耗尽,申请一个新的BlockChunk
    if (block_chunk == NULL || block_chunk->idx_ >= kBlockChunkSize) {
        if (NewBlockChunk()) {
            block_chunk = block_manager_.block_chunks_.back();
            size_t res_idx = block_chunk->idx_;
            block_chunk->idx_++;
            pthread_mutex_unlock(&block_mtx_);
            return &block_chunk->blocks_[res_idx];
        }
        else {
            pthread_mutex_unlock(&block_mtx_);
            return NULL; 
        }
    }
    // 如果有空闲的Block那么直接分配
    else {
        size_t res_idx = block_chunk->idx_;
        block_chunk->idx_++;
        pthread_mutex_unlock(&block_mtx_);
        return &block_chunk->blocks_[res_idx];
    }
    pthread_mutex_unlock(&block_mtx_);
    return NULL;
}

(2)对象池释放资源流程


在 Global Pool 的析构函数中,遍历 BlockChunk 数组,将所有的 BlockChunk 释放掉,这样做的优点是对象池中的资源统一管理不会出现内存泄露的问题,即便存在没有回收的对象。缺点是在整个过程中对象池所占用的内存都没有释放,如果出现分配对象数量峰值高但后面并不需要这么多对象时会出现较多的内存浪费。

~GlobalPool() {        pthread_spin_destroy(&freeslots_mtx_);        pthread_mutex_destroy(&block_mtx_);
        for (int i = 0; i < block_manager_.block_chunks_.size(); i++) {            delete block_manager_.block_chunks_[i];        }    }

9. Object Pool

在 Object Pool 中提供了访问对象池的接口,其中维护了 Global Pool 和 Local Pool,在新建 Local Pool 时使用 round-robin 算法给 Local Pool 分配对应的 Global Pool。

class ObjectPool {   GlobalPool<T> global_pool_[kGlobalPoolNum];    thread_local static LocalPool<T> *local_pool_;    std::atomic pool_idx_;}

实现过程中涉及到的一些问题:

(1)内存对齐

使用 __attribute ((aligned(64))) 与 cacheline 进行对齐,内存对齐可以避免 cacheline 的伪共享。伪共享是什么?现在的 CPU 一般有三级缓存,其中在 CPU 中 L1 cache 和 L2 cache 为每个核独有,L3 则所有核共享,因此产生了 cache 的一致性问题。为了解决 cache 的一致性问题,一个核在写入自己的 L1 cache 后,另一个核对在同一个 cacheline 上的变量进行访问/修改时需要根据 MESI 协议把对应的 cacheline 同步到其他核,从而保证 cache 的一致性。但是在多线程程序中有可能发生下图中的现象:被不同线程访问、修改的变量被加载到同一 cacheline 中。

当多核要操作的不同变量处于同一 cacheline,其中一个核心更新缓存行中的某个变量时,这个 cacheline 会被标为失效,如果其他核心需要访问这个 cacheline 时需要从内存中重新加载,这种现象被称为伪共享。

如果在伪共享的情况下对该 cacheline 上的变量频繁读写,会产生大量的 cache 同步开销。为了避免伪共享,可以通过 cacheline 填充使得该 cacheline 是专属于某个核的。在对象池中的数据结构类如 Local Pool、Global Pool 都使用了 cacheline 对齐,防止在访问这些数据时被其他的变量所影响,这是一种用空间换时间的方法。下面是对象池中对 Local Pool 和 Global Pool 进行内存对齐的例子:

struct __attribute__((aligned(64))) LocalPool {    GlobalPool<T> *global_pool_;    Block<T> *block_;    FreeSlots<T> freeslots_;};
class __attribute__((aligned(64))) GlobalPool {    BlockManager<T> block_manager_;    FreeSlotsManager<T> freeslots_manager_;

    pthread_spinlock_t freeslots_mtx_;    pthread_mutex_t block_mtx_;};

由于 Local Pool 和 Global Pool 中的成员变量在对象池进行分配和释放的过程中会被频繁访问,如果不进行内存对齐有可能会发生伪共享产生较大的性能损失,因此这里通过内存对齐来避免伪共享。


进行内存对齐后耗时减少约 5%。

(2)锁优化


锁优化的手段一般有这几种:

  • 减少锁持有的时间 

  • 减少锁粒度

除了这两种手段外当然最好就是能避开锁,thread local 的资源池就是比较典型的例子。减少锁持有的时间就是缩短临界区,尽量将可以不在临界区中进行操作的语句移到上锁的区域之外。减少锁的粒度就是一把大锁划分为多个小锁,这样就可以使得加锁的成功率得到提高,达到优化性能的目的。

在实现的对象池中主要是将对 Global Pool 的锁划分为对 FreeSlotsManager 以及 BlockManager 这两把锁,但是具体用什么锁呢?这需要根据 FreeSlotsManager 和 BlockManager 中操作的临界区特点来决定,首先看下 Mutex 和 SpinLock 的区别:

  • 对 Mutex:尝试获取锁,如果可得到就占有,如果不能就进入睡眠等待,缺点是会产生 context switch 和 scheduling 开销;

  • Spin Lock: 尝试获取锁.如果可得到就占有,如果不能持续尝试直到获取,spin lock 的 lock/unlock 性能更好(花费更少的 CPU 指令),缺点是在获取到锁前线程不做任何事情,相当于 CPU 一直在空转,造成了算力的浪费。


由上面的特点可知:Spin Lock 适用于临界区运行时间很短的场景,Mutex 适合运用于临界区运行时间较长的场景,对于 Global Pool 中的 FreeSlotsManager 更适合使用 Spin Lock,因为在发生资源交换时临界区操作比较轻量级,只涉及到简单整型数值的比较以及加法。而 BlockManager 部分有可能需要申请一大片内存,临界区消耗大使用 Mutex 比较合适,根据临界区耗时使用不同类型锁后耗时降低9%。

 

(3)分支预测优化

使用 __builtin_expect 控制分支预测结果,__builtin_expect() 是 GCC 提供给开发人员使用的一种将分支转移的信息提供给编译器的手段,这样编译器可以根据所提供的分支转移信息可以对代码进行优化,以减少指令跳转带来的性能下降。


下面是 __builtin_expect 的使用方法:

  • __builtin_expect((x),1) 表示 x 的值为真的可能性更大;

  • __builtin_expect((x),0) 表示 x 的值为假的可能性更大。

因此可以在一些 if 语句中嵌入 __builtin_expect 来对分支预测进行优化,如在 Global Pool 中,当 Block 用光时需要 new 一个新的 BlockChunk,分配失败的概率是非常小的,因此可以这样写:

 BlockChunk<T> *new_block_chunk = new (std::nothrow) BlockChunk<T>; if (unlikely(new_block_chunk == NULL))     return false;

分支预测优化后耗时减少约2%,效果一般,原因可能是分支预测器已经做得很好了,所以手动提供分支转移的信息提升的也不多。

(4)如何在复用的内存上调用对象的构造和析构函数

可以结合可变模板参数、placement new、std::forward 这几个特性来实现在复用的内存上调用对象的构造和析构函数,通过可变模板参数和 std::forward 来传递参数,通过 placement new 在固定的内存上调用构造函数,析构函数直接通过指针直接调用即可,代码如下:

    template<class... Args>    void Construct(T *p, Args&&... args) {        new (p) T (std::forward<Args>(args)...);    }

    void Destory(T *p) {        p->~T();    }

    template<class... Args>    T* New(Args&&... args) {        T *p = Allocate();        Construct(p, std::forward<Args>(args)...);        return p;    }

    void Delete(T *p) {        if (p) {            p->~T();            Free(p);        }    }

六、测试

1. 如何证明对象的内存被有效分配了?

对所分配出来的对象进行读写,最后在所有对象分配结束后验证对象的值是否发生改变。

2. 如何证明对象被成功复用?

先批量分配 n 个对象,然后回收这 n 个对象,检查此时的进程使用的内存使用量,下一次继续分配 n 个对象,如果此时的进程内存使用量没有改变,那么说明这些对象是成功复用的。

3. 内存泄露测试

使用 valgrind 工具进行内存泄露测试:

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

4. 操作开销定位

使用 perf 工具进行记录,函数开销图如下:

5. 耗时测试

耗时测试是从内存分配效率的角度来进行测试,测试的对象是 POD,因为相同类型的对象的构造析构的成本在不同的对象池中是相同的,在测试过程中需要降低对象的构造和析构对内存分配结果的影响。耗时测试主要与 brpc obejct pool、glibc malloc/free、jemalloc 的内存分配、释放效率进行对比。


测试场景:每个对象的大小为 64 Byte,使用 thread_num 个线程访问对象池,每个线程每轮分配 10w 个对象,打乱对象后进行回收,重复 50 轮。比较的对象为 glibc malloc/free, jemalloc, brpc object pool,设置的 global pool 的数量为4。


下图是线程数量为 1-4 时各个分配器的耗时对比图,其中在线程的数量为(1-4)时较其他三个分配器都有较大的优势,相对于其他分配器都有超过 50% 的耗时减少。

下图是访问线程数量为 1-16 时各个分配器的耗时曲线图,在线程数较多的情况下 object pool 与 jemalloc 较 glibc malloc/free 以及 brpc object pool 都有较为明显的优势,其中 object pool 相对 glibc malloc/free 耗时减少 60%,相对 brpc object pool 耗时减少 69%。object pool 在线程较少时相较于 jemalloc 有明显优势,但是随着访问的线程逐渐变多这种差距逐渐缩小了。

另外还需要说明的是在 brpc 官方文档中称:brpc object pool 稳定好于 glibc malloc/free,根据实测在分配的轮次较少的情况下的确是这样的,但是在复用轮次变多时性能变差,个人认为的原因是:brpc object pool 在 thread local 的资源池的实现中,对于那些空闲的对象使用了一个指针数组来保存,在进行资源交换时使用 memcpy 来拷贝空闲对象的指针使得效率非常低,这种劣势在复用轮次变多时被放大了。

在内存上相对于与 brpc object pool 的内存消耗大致在同一水平,使用 16 个线程访问对象池,每个线程分配 10w 个对象,然后进行回收,然后查看进程的 VmRSS, object pool 使用的内存为 120M,brpc object pool 使用 132M。

相关推荐

大模型的Agent Skill功能,在LLM HTTP底层交互流中是怎么承载的?

网络文章文本的 skill,昨天我实现了之后也就开始好奇起 Skills 的底层原理了——相比起各种花里胡哨的 agrnt 应用,我一直以来都很对大模型在纯粹的 HTTP 交互层面是如何交互的更感兴趣。Skill 的全部"魔法"都发生在 system prompt 的措辞和 SKILL.md 文件的编写质量上——本质上是一种 prompt engineering + 文件系统的组合。但是各种天花乱坠的 skills,是如何在底层的 HTTP 中,与大模型——这个只会嘴遁的工具交互的呢?

QcloudCommunity的博客 407

万字入门AI Infra:深入理解大模型中的数学与Infra优化

关注腾讯云开发者,一手技术干货提前解锁👇AI Infra科普、入门或者进阶 ~~ 不到 5 万字,带你融会贯通理解大模型的数学、算法、硬件到系统工程。任何问题欢迎沟通交流。可怕,我竟然有点认同这句话。当然大概率是因为姚顺宇太懂AI了,而我则是因为无知者无畏。不过仔细想想,大模型推理中的数学逻辑确实不复杂,很多原理只需要高中数学知识就能看懂。但是为什么像 vLLM 这种推理系统这么复杂呢?本文将拆解大模型中几个核心操作(RMSNorm、Softmax、Causal Mask、Sampling)背后的数学与

QcloudCommunity的博客 1168

平平无奇的源码,竟藏着Agent的核心秘密?

System Prompt 是剧本,Skill 是剧本的扩展包,Agent Loop 是演员按剧本行动。欢迎留言,我们将邀请作者针对性回复你的评论,我们将选取1则优质的评论,送出腾讯云定制文件袋套装。,让 Agent 能主动"巡逻",而不只是被动等待用户消息。OpenClaw 的 System Prompt 分层结构。的"行为底线",任何 Skill 或用户命令都无法覆盖它。精华 2:Skill 的"按需加载"避免 token 浪费。精华 1:System Prompt 是"分层乐高积木"

QcloudCommunity的博客 426

QQ音乐Harness Engineering实践

一个看似简单的需求,背后可能牵动服务调用链、配置、灰度、埋点、错误码、接口契约和历史兼容性。当 AI 开始快速生成大量代码,真正的瓶颈就不再是"写不出来",而是"看不完、想不清、管不住"。在对话式编码阶段,一种典型的工作模式逐渐流行:开发者把需求丢给 AI,扫一眼输出"看起来对"就直接采纳,不审 diff、不追问逻辑、不验证边界。以音乐商业化业务为例,一个需求可能从 TAPD 单开始,经过需求评审、技术方案、服务影响面分析、IDL 契约变更、业务代码实现、测试验证、CR、灰度上线,最后才进入稳定运行。

QcloudCommunity的博客 715

从Prompt、Context到Harness,工程的三次进化与终局之战

同时,强制要求散落各处的决策记录(Slack、邮件、文档)全部迁移至代码仓库,确保 Agent 的唯一信息来源是可信的、版本受控的仓库。更坑爹的是,研究表明,当上下文过长时,模型会出现"中间遗忘(Lost in the Middle)"现象:它对开头和结尾的内容记忆较好,对中间大段内容的关注度大幅下降。这篇文章,我想带你完整走一遍这三次进化的逻辑:它们分别解决了什么问题,它们之间是什么关系,它们的边界在哪里,以及:当三者融合,AI 工程师的终极形态究竟是什么?大语言模型的本质,就是这位金鱼助理。

QcloudCommunity的博客 719

让Skill自己训练自己:8阶段Loop、3层评测、5维AND门控,从此实现自进化

所以每一轮改动都是有据可查的,不是「感觉这里可以优化」,而是「Case 41 挂了,因为 agent 把离职问题路由到了邮箱分类,但正确答案在通讯录,所以我改 root_index.md 加个易混淆提示」。但让我最后决定把它记录下来的,不是这的个「吸收」过程,而是它真的在替我探索我不想费脑力触达的边界,这看起来更像是一个机器智能应该有的样子。不准跨层,每轮只改一层的东西。但我经过这 19 轮之后觉得,更准确的描述是互补——你在明处看着,AI 在暗处替你试错,你看不见的那一半就是 AI 能贡献的地方。

QcloudCommunity的博客 1127

只加两行代码,为什么要两天?一文深度理解业务系统的复杂性

关注腾讯云开发者,一手技术干货提前解锁👇业务系统复杂性一直是令开发者头痛的问题。复杂的不是增加一个需求需要耗费多少时间,而是在增加一个需求后带来的蝴蝶效应:其它功能会不会受到影响、要如何去找到这些影响,最终如何实现系统正常运行......功能之间隐秘增加的耦合、不可避免的代码腐化在导致业务复杂性增加。大家都在说的软件开发提效到底在提什么?程序员日常工作中应该如何提升开发效率?敏捷开发、瀑布流式开发孰是孰非?欢迎阅读。业务背景与目标我依然非常清晰地记得某个时候,我所在团队 Leader 曾讲过一段话:“我担

QcloudCommunity的博客 713

从零设计生产级 Multi-Agent Harness:架构、评估、记忆、成本与 MCP 工具接入全拆解

在 AI Agent 领域,Multi-Agent Harness 指的是把多个 Agent 的能力、工具、状态、通信、编排、监控统一收束在一个运行时之内的框架。第三,Agent 路由。一个输入框,一个大模型,几个工具,一段写得很漂亮的 System Prompt,再配上一个炫酷的前端,看起来就像一个能干活的“数字员工”。Multi-Agent 完全不一样——它有计划、有中间步骤、有工具调用、有 Agent 协作、有重试、有反思、有最终合成。增加 Budget、权限、重试、压缩、轨迹评估、审计、回归测试。

QcloudCommunity的博客 710

拆完Hermes源码,我发现Agent的“自我进化“根本不需要训练模型

两个月 5.2 万 Star,Hermes Agent 用一个"LLM 审判官"机制实现了 Agent 的自我进化——不改模型权重,只改"怎么用模型"的策略。OpenClaw(社区昵称"龙虾"🦞)是 2025-2026 年最火的开源 Agent 框架,30 万+ Star,插件市场 ClawHub 里有数千个插件,能接入 Telegram、WhatsApp、Slack、飞书、企业微信——几乎是一个"个人 AI 操作系统"。Skill 是英语写的,Memory 是英语写的,审查 Prompt 也是英语写的。

QcloudCommunity的博客 909

AI Infra 其实没有多少新东西

传统 Infra 追求横向扩展,而 AI Infra 呈现 “AI 大型机”特性,是因为传统后台服务的可以容忍毫秒级延迟,但 AI 集群不行,GPU 的算力是 CPU 的数百倍,微秒级的延时等待也会造成很大的算力损耗,需要硬件的高度集成。在可预见的1-3年的未来,这样的专用硬件+网络的集中式架构很难发生比较大的改变。与之相似,模型推理的批处理就是将多个输入样本打包(batch),将原本串行的 N 次轻量的推理计算,合并为 1 次重量的计算,实现单位时间内处理更多的请求,提高了 GPU 利用率。

QcloudCommunity的博客 825

程序员越早想通这些越好

永远没有正确架构,你永远无法偿还完所有的技术债务,如同你永远不会设计出完美的界面,请避开完美主义的陷阱,永远没有完美的代码,能够满足当前需求,又为未来不管是自己还是他人留有扩展空间,就很好了。我们努力的方向就是将现实世界映射到有限的数据结构中,这就需要用户自定义类型,如果你的代码中包含类似 trader portfolio 这样的概念,你就可以使用他们的名称来建模,而不是使用基本数据类型复杂嵌套表述你的目的。从另一个角度看,代码里的注释都是补丁,是碎片化的,局部的注释很难推导模块整体的顺序及耦合关系。

QcloudCommunity的博客 466

RAG已死?不,是Grep回归了!

这样一来可以直接复用已有的计算缓存,只为新增的增量部分付全价,前面累积的大头按约 1/10 的缓存价计费。所以实际的工具组合方式是灵活的:Grep(默认模式)→ Read 是最常见的路径,但 Grep(content 模式)可以独立使用,LLM 也可以直接调 Read(如果已经知道文件路径),或者一次同时发起多个 Grep 并行搜索。它从零开始构建自己的对话历史,不继承主对话的消息,这意味着它搜索过程中产生的大量 grep 结果、代码片段都留在自己的 context 里,主对话只收到一段总结性的文本结论。

QcloudCommunity的博客 576

深入浅出Harness Engineerring之核心模式与理念

在智能体生命周期的关键节点(如会话开始、工具调用后)自动触发预设动作(如代码格式化),由系统确保关键流程被执行,不依赖可能被模型遗忘的指令。简单讲就是利用向量存储这个技术,来实现模糊检索,原理是:即使字面不同,但语义相近的文本,其向量在数学空间中的位置也很接近。根据命令类型、参数和影响,自动评估其风险等级(安全、有风险、危险),并采取自动执行、请求确认或直接拦截等不同策略。关键特性是可隔离、可重建、可扩展。不是一次判断就定终身,允许你改变、允许情况复杂,通过不断观察、思考、调整,越来越懂真实的你。

QcloudCommunity的博客 624

Harness Engineering实践心得:如何高效驾驭AI?

但如果这些经验被拆成一条条 Rule、一个个 Skill、一项项 Script 检查——"XAML 不能有中文"、"编译要用这个路径的 MSBuild"、"SVN 命令必须带认证参数"——AI 就能在秒级时间里"吸收"十年的踩坑经验。然后可以看到下方的图,我的PM会很顺利的调度其他agent分别负责不同的事项,遇到问题的时候也能准确的找到合适的agent。MCP 本质上是在扩展 AI 的"感知范围"——Rule 管"该怎么做",Skill 管"怎么操作",MCP 管"能看到什么、能碰到什么"。

QcloudCommunity的博客 842

从第一性原理思考 Agentic Engineering

需要注意的是,这一结论是有条件的:如果 AI 缺乏领域知识,过早介入反而可能引入 1.2 节所述的"似是而非"损耗——因此上下文供给(实践 1)是 AI 全链条参与的前提,而非结果。通过一个专门的自迭代 Skill 实现——当 AI 在协作过程中识别到自己被纠正(用户否定了 AI 的输出并给出了正确方向),它自动评估这个错误的根因,搜索现有的 Rules 和 Skills 中是否已有针对该类问题的处理规则。事实上,越是复杂的项目,工程师的认知负担越重(公理 3),AI 分担认知负担的潜在边际价值也越大。

QcloudCommunity的博客 757

万字干货!Harness Engineering如何工程化落地?

关注腾讯云开发者,一手技术干货提前解锁👇Harness Engineering 的概念已经火了有一阵了,全网很多文章基本都是在讲理念,讲为什么今天做 AI 开发,不能只靠一段提示词,也不能把模型当一个“更聪明的代码补全”。但与此同时,一个问题也在持续地发酵:这个问题特别重要。因为 Harness 这个词听上去很大,很像一个抽象的方法论,但它如果不能落到工程、目录、文档、脚本、工作流里,那它最后就只是一句漂亮的话。事实上,不同项目落地出来的 Harness,表面上可能完全不一样。有的项目是 CI 很强,有的

QcloudCommunity的博客 1170

全方位对比:Hermes VS OpenClaw

多账号矩阵运营 如果你的业务涉及同时管理多个社交媒体账号、多个平台的内容分发、多渠道的客户互动——OpenClaw是目前市场上的"无可替代的底座"。一个有专业背景的个人,现在可以快速构建一个垂直领域的AI助手:比如"财务分析Hermes"、"法律咨询Hermes"、"技术架构Hermes",直接通过微信提供服务,积累自己的专业IP。跨平台生态融合 如果你的工作流涉及飞书、Slack、Discord等多个平台的深度集成,需要这些平台之间的数据流转和联动——OpenClaw的生态支持是强大的。

QcloudCommunity的博客 1217

一文讲透:Harness Engineering即控制论!

而研发模式的升级我认为将会是一个飞跃的过程,之所以现在我们仍然需要需要参与编码,一个非常大的原因就是缺乏传感器和基建,仍然需要人来串流程,比如需要人来验证UI是否正确、需要人来告诉AI代码不符合代码规范,因此在有些场景我们仍然感觉 AI 完成的很吃力,但这不是 AI 能力不行,而是我们并没有为 AI 提供友好的环境……控制论中的突变理论介绍了矫枉过正和飞跃现象之间的矛盾,如果质变中经历的中间过渡态是不稳定的,那么他就是一个飞跃的过程,如果中间过渡态是稳定的,那么他就是一个渐变的过程。但是这些阶段要解决的。

QcloudCommunity的博客 1204

神级开源插件!一句话让QClaw变3D小镇,Agent工作可视化,赛博云监工

这个架构是翻了几次车才想通的—原有AI的实现机制是用 setTimeout 猜动画时长导致了幽灵 NPC、工位撞车、越改越崩,后面我停下来换了思路,推翻原有的架构,按照“意图+回传”的架构进行重构,之前的问题一次性全部解决。15 天,我和 AI 吵了 185 次架, 4.5 万行代码,开发了一款 QClaw / OpenClaw 的UGC游戏插件,这可能是人类跟 Agents 交互的新范式,插件已完全开源,有需要的欢迎自取~我天天用,用着用着我就在思考,Agent 产品现在的交互体验,基本都一个样。

QcloudCommunity的博客 1197

一文搞懂Hermes:新顶流Agent如何从经验中自我进化

Voyager 是一个能在 Minecraft 中自主探索、学习技能、无限积累能力的 AI Agent,它提出了 "Skill Library"(技能库)的概念——Agent 将成功的行为序列编码为可复用的技能函数,存储在一个持续增长的代码库中。如果 Skill 的名称和描述不够精准,或者用户的任务描述与 Skill 的触发条件有语义差距,Agent 可能会错过相关的 Skill。如果进程在写入过程中崩溃,目标文件要么是旧内容(还没被替换),要么是新内容(替换已完成),绝不会出现写了一半的损坏文件。

QcloudCommunity的博客 2635

自掏腰包一万元,拥抱AI这一年,我的工具、实践和思考

其实也很好理解,过往的每个领域(比如前端、后端、设计、测试等等)都主要是这个领域的从业者在为它添砖加瓦,但是大模型时代的 AI 应用领域却几乎是所有使用 AI 的人类一起在迭代,无分领域,无分文理,无分老少。因为每个时期的 AI 都有着不同的特点,很多时候当我费尽心思去钻研,自以为有点弄明白 AI 应该怎么玩的时候,基座模型和行业范式常常就会迎来颠覆式的变革,很多过往的经验都会倾覆,然后只能从头再来。过去一年多,被热议的 AI 范式大概是沿着四个阶段递进演化,每一个阶段其实都在解决前一个阶段的核心问题。

QcloudCommunity的博客 627

办公Agent的CI/CD时刻到来了

核心观点是:企业给员工配了一堆 AI 工具,试点跑了几百个,但真正拖慢 AI 转型速度的瓶颈不在模型能力,在"最后一公里":AI 输出和实际工作流之间的那道鸿沟。代码写完,打个 tar 包,开 FTP 传到服务器上,SSH 进去解压,改配置文件,重启服务,刷一下页面祈祷别 500。你复制,切窗口,粘贴到腾讯文档里,发现 Markdown 格式全乱了,手动调标题层级,代码块重新标记语言类型,表格散架了重排一遍。模型是越来越聪明了,但"生成"到"落地"之间那段路,还是你自己在走,一步一步,手动搬运。

QcloudCommunity的博客 608

4亿token买来5个教训:让6个AI Agent连写4天代码发生了什么?

Watchdog 检测到崩溃 → 启动诊断 Agent 分析原因 → 诊断 Agent 跑满 20 轮没搞定 → Watchdog 判定“已修复”,重启系统 → 重启的系统因为诊断 Agent 留下的环境变量被继承,连续 3 次拒绝启动 → 触发连续失败保护退出 → Watchdog 第 3 次巡检,发现进程和记录文件都没了,尝试手动重启,自己也停了。启动前我翻了论文、做了架构设计、写了监控+修复异常的脚本。这个发现让我的情绪从沮丧变成了兴奋——不是那种"终于修好了"的释然,是"我找到根因了"的快感。

QcloudCommunity的博客 584

赛博斗蛐蛐:9大模型决战三国志,天命在谁?

我猜是这样的,当Gemini面临决策的时候,一系列关键词比如 守城,劣势,粮草,三国等等,在语义层面的关联性中LLM发现了竖壁清野这个关键词概率非常高,所以以此为策略,然后再根据这个策略完美执行操作。MiMo 的外交是三方中最精明的。Kimi作为国产模型,也同样颠覆了我对他的看法,最后的比赛他甚至一度是优势者,存在很多夺冠的可能性,要知道他的对手可是claude和gemini,而他经常出兵慢,回过头来想想或许正是他的策略,毕竟中后期他的优势都来自于稳健的发展和较少的战斗频率(和claude比).

QcloudCommunity的博客 874

Claude Code是怎么知道你在骂他的?这 12 条发现值得关注

确实存在——5,500+ 行,集成了消息队列、工具装配、MCP 编排、会话状态、权限、分析、文件持久化和优雅关闭,基本上一个文件干了所有事。对抗虚假声明——"不要在输出显示失败时声称所有测试通过,不要压缩或简化失败的检查来制造绿色结果"。完整的 KAIROS 是一个持续驻留的守护进程,能感知你在不在电脑前、主动寻找有用的工作做,更像是住在终端里的全职同事。源码里藏着一个叫 "Buddy" 的电子宠物系统,完整度令人惊讶——这不是一个 TODO,而是一个从数值系统到 ASCII 动画都实现了的完整功能。

QcloudCommunity的博客 701

逆向深扒Claude Code源码,我发现了什么!?

Anthropic 内部用户获得更严格的质量控制 prompt、更好的验证机制(独立 Verification Agent)、以及针对 model 弱点的专门对策(如 Capybara v8 的 29-30% 错误声明率对策)。这不是代码混乱的标志——它是整个 Agent Loop 的心脏,所有的流式处理、工具调度、上下文压缩、子代理管理都在这个文件中协调。这种"胖核心"设计是刻意的:保持核心循环的原子性,避免分散到多个模块导致的协调复杂度。核心价值:Fork 代理的工具输出不会回到主代理的上下文窗口。

QcloudCommunity的博客 892

一文讲透如何构建Harness——六大组件全解析

本文将从"裸模型的四个硬伤"出发,逐一拆解 Harness 的六大组件——文件系统、Bash + 沙箱、记忆(AGENTS.md)、Web Search + MCP、上下文工程、编排 + Hooks——带你看清 AI Agent 真正的竞争壁垒,不在模型层,而在 Harness 层。如果你用过 Agent 做复杂任务,你一定有过这样的体验:一开始 Agent 的表现很好,但随着对话变长,它开始"变笨"——遗忘之前的约定,重复之前犯过的错误,甚至对同一个问题给出前后矛盾的回答。对于简单的问答场景,这无所谓。

QcloudCommunity的博客 761

Harness Engineering 来了,SDD 还有意义吗?

他们的解法是把「品味」编码进规则,然后用一个定期运行的「doc-gardening」Agent 自动扫描漂移、发起修复 PR——「技术债务就像一笔高息贷款,不断地以小额贷款的方式偿还,总比让债务不断累积要好」。代码仓库本地的、已版本化的工件,才是 Agent 所能看到的全部。在 Vibe Coding 模式下(即不做规划、随意向 AI 提需求的开发方式),大量设计决策、跨服务约定、「为什么不选另一个方案」的讨论,全部停留在一次性的 AI 对话里——下一个人,或者下一次 AI 会话,完全无从获取。

QcloudCommunity的博客 419

零废话!一文讲透从0构建AI Agent

每个工具都要写一套"定义→解析→执行"的胶水代码。上下文管理就是在"记住足够多"和"别超标"之间找平衡,常见策略包括滑动窗口、摘要压缩、RAG 检索增强。上下文窗口是 Agent 的"工作记忆",但它有硬性限制。LLM 负责"动脑"决策,代码负责"动手"执行。这是从"聊天机器人"进化到"Agent"的关键一步。每次调 API 都把完整对话历史发过去,LLM 本身是无状态的,"记忆"完全靠客户端的数组模拟。这样 Agent 一开始就知道自己"在哪里"、"手头有什么",不需要先花一轮工具调用去探索环境。

QcloudCommunity的博客 711

距离AI把小编我裁掉还有72小时

这是一个基于 OpenClaw 开源生态打造的本地化 AI Agent 助手,已于上周开启全量公测,无需复杂配置,三步即可上手,自带安全防护,支持微信直连绑定,内置多款优质国产大模型,现阶段也是完全免费使用,基本上前面提到过的痛点都可以靠它解决。对了,这里特别强调一点,上面的截图让产品同事汗流浃背,再三找我确认 QClaw 思考过程中优美的“草”字说法,不会是默认出现的吧?AI 好,AI 得学,但更重要的是,在你的工作和生活里,哪些东西是能结构化地被 AI 所接入的,不要死守着那些脏活累活不放。

QcloudCommunity的博客 674
上一篇: 云原生数据湖101
下一篇: 技术拍客约起,云+社区原创视频大奖等你来领
腾讯云开发者
腾讯云开发者 企业官方账号 企业官方账号
博客等级 码龄10年 6291粉丝 1962原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值