[项目设计] 从零实现的高并发内存池(三)

🌈 博客个人主页Chris在Coding

🎥 本文所属专栏[高并发内存池]

❤️ 前置学习专栏[Linux学习]

 我们仍在旅途                     

目录

4.CentralCache实现

        4.1 CentralCache整体架构

        4.2 围绕Span的相关设计

        页号

        Span

        Spanlist

        4.3 向CentralCache申请内存

        CentralCache.h

        FetchFromCentralCache

        FetchRangeObj

5.PageCache实现

        5.1 PageCache整体架构

        5.2 向CentralCache申请内存

        PageCache.h

        GetOneSpan

        NewSpan

申请过程联调单元测试


4.CentralCache实现

        4.1 CentralCache整体架构

  • 哈希桶结构: Central Cache 和 Thread Cache 都采用了哈希桶的结构。这意味着内存块根据其大小被分配到不同的桶中。每个桶都可以独立地管理一定大小范围内的内存块。

  • 锁机制: 由于 Central Cache 是多个线程共享的,所以在访问 Central Cache 时需要考虑线程安全性。一种常见的做法是使用桶锁,即为每个桶分配一个锁。这样在多个线程同时访问同一个桶时,只有该桶的锁会被竞争,而其他桶的访问不受影响,从而降低了锁的竞争程度。

  • Span 结构: Central Cache 中的每个桶中存储的是 Span,而不是单个内存块。Span之间以双向链表的形式链接。Span 是以页为单位的大块内存,它们会被切分为更小的内存块以便分配给线程使用。每个 Span 中还包含一个空闲链表,其中存储了切分后的内存块。这种设计能够提高内存分配的效率,减少内存碎片化。

  • Spanlist: Spanlist 采用双向链表结构组织维护Span内存块,这样可以在 O(1) 的时间内进行Span的分配和释放操作。通过简单地修改指针,就可以将内存块从 Span 的空闲链表中取出或者放回,这样保证了内存分配和释放的高效性。

  • Spanlists与 freelists遵循同一对齐映射规则: 在 Central Cache 中,Spanlists 是存储 Spanlist(Span 的双向链表)的数组,而 freelists 则是存储空闲内存块链表的数组。 它们的下标对应关系设计得非常巧妙,即 freelists 的下标与 Spanlists 中的每个桶对应。这样就能够确保每个桶中的 Span 在 freelists 中有相应的位置,从而方便线程在需要时快速定位到合适的内存块。

  • 内存块的切分: 每个 Span 中的内存块是根据其所在的哈希桶的大小要求进行切分的,以对应下标相同的freelist。这样可以确保每个哈希桶中存储的内存块大小是符合预期的,方便线程在需要时从 Central Cache 中获取适当大小的内存块。

        4.2 围绕Span的相关设计

页号

前面讲到Span 是以页为单位的大块内存,在讲到定长内存池时我们也提过页的大小一般是8K,既然Span是以页为单位,那么我们可以将整个内存从零开始划分成不同的页并编号的形式统一管理。

 这里,我们可以给不同的页编上不同的ID(也就是页号)用于区分

可是页号要用什么类型来表示呢?

- 在32位平台下,进程地址空间的大小是 2^{32}
- 在64位平台下,进程地址空间的大小是 2^{64}
- 页的大小通常是8KB。
- 在32位平台下,进程地址空间可以被分成\frac{2^{32}}{2^{13}} = 2^{19}个页。
- 在64位平台下,进程地址空间可以被分成 \frac{2^{64}}{2^{13}} = 2^{51} 个页。
而size_t类型能表示的数值范围为: 0到2^{32}-1 ,因此我们不能单纯用size_t来实现,这里需要借助条件编译的方式在Common.h中实现不同平台下的页号类型。

#ifdef _WIN64
typedef unsigned long long PAGEID;
#elif _WIN32
typedef size_t PAGEID;
#endif

(在32位下,_WIN32有定义,_WIN64没有定义;而在64位下,_WIN32和_WIN64都有定义)

Span

struct Span
{PAGEID _pageId = 0;size_t _n = 0;Span* _next = nullptr;Span* _prv = nullptr;size_t _used = 0;void* _freeList = nullptr;
};
  • _pageId:记录大块内存起始页的页号,便于后续在Page Cache中进行内存合并。
  • _n:记录该Span管理的页的数量,这个数量并不是固定的,可能会根据各种因素进行调整。
  • _next和_prev:双链表结构,用于将Span组织成链表,方便进行插入和删除操作。
  • _used:记录被分配给thread cache的内存块数量,当所有内存块被还回时,_used变为0,表示该Span可被归还给Page Cache。
  • _freeList:切好的小块内存的空闲链表,用于存储被切分出来的内存块,以便于后续分配给请求线程。

Spanlist

通过我们之前CentralCache的整体架构,我们知道CentralCache的每个哈希桶里面存储的都是一个双链表结构,对于该双链表结构我们直接定义为Spanlist对其进行封装。其逻辑就是基础的带头双向循环链表,这里不多赘述。需要注意的是我们在这里需要定义好我们的桶锁,因为CentralCache可能会同时有多个线程访问,通过桶锁(也就是只在每个Spanlist加锁)的方式,我们不仅满足了线程安全,同时也降低了锁的竞争激烈程度。

class SpanList //带头双向循环链表
{
public:SpanList(){_head = new Span;_head->_next = _head;_head->_prv = _head;}bool empty(){return _head->_next == _head;}Span* begin(){return _head->_next;}Span* end(){return _head;}void push_front(Span* span){insert(_head->_next, span);}Span* pop_front(){return erase(_head->_next);}void insert(Span* pos, Span* span){//插在pos前面span->_next = pos;span->_prv = pos->_prv;pos->_prv->_next = span;pos->_prv = span;}Span* erase(Span* pos){pos->_next->_prv = pos->_prv;pos->_prv->_next = pos->_next;return pos;}std::mutex _mtx; // 桶锁
private:Span* _head;
};

        

        4.3 向CentralCache申请内存

CentralCache.h

每个线程都有一个属于自己的thread cache,我们是用TLS来实现每个线程无锁的访问属于自己的thread cache的。而central cache和page cache在整个进程中只有一个,我们直接将其设置为单例模式。

#pragma once
#include"Common.h"
class CentralCache
{
public:static CentralCache& GetInstance(){return _SingleInstance;}// ...
private:CentralCache(){}CentralCache(const CentralCache&) = delete;static CentralCache _SingleInstance;SpanList _spanlists[NFREELISTS];
};

为了保证CentralCache类只能创建一个对象,我们将central cache的构造函数和拷贝构造函数设置为私有。CentralCache类当中还需要有一个CentralCache类型的静态的成员变量,当程序运行起来后我们就立马创建该对象,在此后的程序中就只有这一个单例了。

CentralCache CentralCache::_SingleInstance;

FetchFromCentralCache

话接上回,当线程申请某一大小的内存时,如果ThreadCache中对应的空闲链表不为空,那么直接取出一个内存块进行返回即可,但如果此时该空闲链表为空,那么这时ThreadCache就需要向调用FetchFromCentralCache函数来向CentralCache申请内存了。

这个时候我们可以结合本章讲到的CentralCache的整体架构来大致分析  FetchFromCentralCache的调用流程。由于我们是将CentralCache的Span链表与ThreadCache的空闲链表下标一 一对应,我们可以直接传入index。而根据所传入的index下标,CentralCache就可以找到对应的Spanlist链表,从中取得一个Span块,再拿取里面已经切分好的内存块

void* ThreadCache::FetchFromCentralCache(size_t index, size_t align_size)
{size_t batch_num = std::min(_freelists[index].MaxSize(), SizeTable::BatchSizeLimit(align_size));if (_freelists[index].MaxSize() == batch_num){_freelists[index].MaxSize()++;}void* start = nullptr;void* end = nullptr;size_t actual_num = CentralCache::GetInstance().FetchRangeObj(start, end, batch_num, align_size);assert(actual_num > 0);if (actual_num == 1){assert(start == end);return start;}else{_freelists[index].push_range(*(void**)start, end);return start;}return nullptr;
}

慢开始算法反馈调节

void* ThreadCache::FetchFromCentralCache(size_t index, size_t align_size)
{size_t batch_num = min(_freelists[index].MaxSize(), SizeTable::BatchSizeLimit(align_size));if (_freelists[index].MaxSize() == batch_num){_freelists[index].MaxSize()++;}// ...
}

我们在SizeTable中加入对该大小的内存块单次申请个数的上限计算函数BatchSizeLimit()

class SizeTable
{
public:// ...static size_t BatchSizeLimit(size_t size){size_t maxnum = MAXSIZE / size;if (maxnum < 2){maxnum = 2;}else if (maxnum > 512){maxnum = 512;}return maxnum;}// ...
};

我们在FreeList中加入该空闲链表内存块单次申请个数的上限函数

class FreeList
{
public:// ...size_t& MaxSize(){return _MaxSize;}// ...
private:// ...size_t _MaxSize = 1;// ...
};
  • 通过慢开始算法最开始不会一次向CentralCache一次批量要太多,因为一次性要太多了可能用不完,造成空间资源浪费
  • 随着向CentralCache申请内存的次数增加,该空闲链表每次可申请内存块的最大值_MaxSize就会随之增大,相应的每次申请到的batch_num就会不断增长,直到上限
  • align_size越大,一次向central cache要的batch_num就越小,可以避免过多内存被浪费。
  • align_size越小,一次向central cache要的batch_num就越大,可以减少频繁申请内存的次数,提高资源利用率。

push_range()

这里我们假设FetchRangeObj()为我们申请回了一串内存块链表,这里我们只需要返回一个内存块返回供外部调用,而多余的则需要插回到我们发起申请的空闲链表中,于是我们继续在FreeList中加入push_range()来满足

class FreeList
{
public:// ...void push_range(void* start,void* end){*(void**)end = _FreeList;_FreeList = start;}// ...
};

FetchRangeObj

size_t actual_num = CentralCache::GetInstance().FetchRangeObj(start, end, batch_num, align_size);

这里我们要从central cache获取n个指定大小的对象,这些对象肯定都是从central cache对应哈希桶的某个span中取出来的,因此取出来的这n个对象是链接在一起的,我们只需要得到这段链表的头和尾即可,这里可以采用输出型参数进行获取。

size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batch_num, size_t align_size)
{size_t index = SizeTable::Index(align_size);_spanlists[index]._mtx.lock();Span* span = GetOneSpan(_spanlists[index], align_size);size_t actual_num = 1;start = span->_freeList;end = start;while (*(void**)end != nullptr && actual_num < batch_num){end = *(void**)end;++actual_num;}span->_freeList = *(void**)end;*(void**)end = nullptr;span->_used += actual_num;_spanlists[index]._mtx.unlock();return actual_num;}
  • 首先,根据 align_size 计算出对应的 index,用于确定在 _spanlists 数组中的位置。
  • 获取到对应 _spanlists[index] 的互斥锁,确保在多线程环境下的安全访问。
  • 调用 GetOneSpan 函数(后面实现)从 _spanlists[index] 中获取一个 Span 对象,用于管理内存块。
  • 通过遍历 Span 对象的自由链表,获取 batch_num 个内存块的范围。
  • 将获取到的内存块范围的起始地址和结束地址分别保存在 start 和 end 中,并同时更新 actual_num 记录实际获取的内存块数量。
  • 更新 Span 对象的相关信息,包括更新 freeList 指针、增加 used 计数等。
  • 释放对 _spanlists[index] 的互斥锁,确保其他线程能够访问该 _spanlists[index]。
  • 返回实际获取的内存块数量 actual_num。

5.PageCache实现

        5.1 PageCache整体架构

  • 哈希桶结构: PageCache 也采用哈希桶的结构,用于管理不同大小的内存页块。每个哈希桶对应一定数量的页数范围,例如一个哈希桶可能管理 1 页的内存块,另一个哈希桶可能管理 2 页的内存块,以此类推。

  • Span 管理: 类似于 CentralCache,PageCache 中的每个哈希桶也挂载着一系列的 Span 对象。这些 Span 对象用于管理连续的内存页块,记录了页块的起始地址和大小。

  • 内存分配与释放: PageCache 负责管理大块内存页的分配和释放。当 CentralCache 需要大块内存页时,它会向 PageCache 发送请求。PageCache 根据请求的页数找到对应的哈希桶,从中获取一个 Span,并将该 Span 切分成适当数量的页块返回给 Central Cache。当页块被释放时,它们会被合并成更大的连续内存页块,以提高内存利用率。

  • 内存大小与桶号映射: 与 CentralCache 不同,PageCache 的哈希桶映射规则采用直接定址法。例如,第 1 号哈希桶中挂载的都是 1 页的 Span,第 2 号哈希桶中挂载的都是 2 页的 Span,依此类推。这样设计的好处是能够简化哈希桶的访问,便于根据请求的页数快速定位到对应的哈希桶。

  • 线程安全处理: 由于多个线程可能同时访问 PageCache 的不同哈希桶,为了保证数据的一致性和安全性,PageCache 使用一个大锁来保护整个 PageCache,确保在任何时候只有一个线程可以对 PageCache 进行访问。

        5.2 向CentralCache申请内存

按照上述整体架构,PageCache中列表的个数是128个(这里为了满足数组下标直接对应,我们定义成129),以及页面位移 (页的大小 = 1<<PAGESHIFT)

static const int PAGESHIFT = 13;static const int NPAGES = 129;

同时此时加上我们在实现定长内存池时封装的申请释放内存接口

#ifdef _WIN32
#include <windows.h>
#endif
inline static void* SystemAlloc(size_t kpage) {
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32VirtualFree(ptr, 0, MEM_RELEASE);
#endif
}

PageCache.h

page cache在整个进程中也是只能存在一个的,因此我们也需要将其设置为单例模式。

#pragma once
#include"Common.h"
class PageCache
{
public:static PageCache& GetInstance(){return _SingleInstance;}std::mutex _pagemtx;
private:PageCache(){}PageCache(const PageCache&) = delete;static PageCache  _SingleInstance;SpanList _Spanlists[NPAGES];
};

当程序运行起来后我们就立马创建该对象,在此后的程序中就只有这一个单例了。

PageCache PageCache::_SingleInstance;

GetOneSpan

Span* CentralCache::GetOneSpan(SpanList& list, size_t align_size)
{// 查看当前的spanlist中是否有还有未分配对象的spanSpan* it = list.begin();while (it != list.end()){if (it->_freeList != nullptr){return it;}else{it = it->_next;}}list._mtx.unlock();//没有一个现成能使用的span对象,多申请几个spanPageCache::GetInstance()._pagemtx.lock();Span* span = PageCache::GetInstance().NewSpan(SizeTable::PageSizeLimit(align_size));PageCache::GetInstance()._pagemtx.unlock();//把span切好后挂到自己的_freelist上void* start = (void*)(span->_pageId << PAGESHIFT);size_t bytes = span->_n << PAGESHIFT;void* end = (char*)start + bytes;span->_freeList = start;void* cur = start;void* tail = (char*)start + align_size;while (tail < end){*(void**)cur = tail;cur = tail;tail = (char*)tail + align_size;}*(void**)cur = nullptr;list._mtx.lock();list.push_front(span);return span;
}

  1. 遍历 SpanList: CentralCache 首先查看指定的 SpanList 中是否有未分配对象的 Span。它会遍历 SpanList,逐个检查每个 Span,如果找到了有空闲对象的 Span,就立即返回该 Span。

  2. 申请新的 Span: 如果 SpanList 中没有可用的 Span,CentralCache 就需要向 PageCache 申请新的 Span。它会通过调用 PageCache 的 NewSpan 方法来申请一个新的 Span。在申请 Span 之前,它会先锁住 PageCache,以确保并发情况下的线程安全。

  3. 切分 Span: 申请到新的 Span 后,CentralCache 需要将该 Span 切分成适当大小的内存块,以供后续分配给线程。它会将 Span 切分成多个对象,每个对象的大小由参数 align_size 决定。

  4. 挂载到 SpanList: 切分完毕后,CentralCache 将该 Span 添加到对应的 SpanList 中,并将其挂载到链表的头部,以确保在下次申请时能够快速访问到该 Span。

  5. 返回 Span: 最后,CentralCache 返回新申请或已有的 Span 给调用方,以供线程分配使用。

总的来说,CentralCache 申请一个 Span 的过程包括查找现有 SpanList、申请新的 Span、切分 Span、挂载到 SpanList,并最终返回 Span。这个过程确保了线程在需要内存时能够及时获得所需的 Span 对象。

PageSizeLimit()

class FreeList
{
public:// ...static size_t PageSizeLimit(size_t align_size){size_t batch = BatchSizeLimit(align_size);size_t npage = batch * align_size;npage >>= PAGESHIFT;if (npage == 0)npage = 1;return npage;}// ...
};

就像ThreadCache一次向CentralCache申请对象的个数上限,现在我们是根据对象的大小计算出CentralCache一次应该向PageCache申请几页的内存块。   

我们用单次内存块申请的最大值乘以内存块的大小,再转化为页数去向堆申请内存。也就是说,CentralCache向PageCache申请内存时,要求申请到的内存尽量能够满足ThreadCache向CentralCache申请时的上限。

NewSpan

Span* PageCache::NewSpan(size_t k)
{if (!_Spanlists[k].empty()){Span* kspan = _Spanlists[k].pop_front();return kspan;}for (size_t n = k + 1; n < NPAGES; n++){if (!_Spanlists[n].empty()){Span* nspan = _Spanlists[n].pop_front();Span* kspan = new Span;kspan->_pageId = nspan->_pageId;kspan->_n = k;nspan->_pageId += k;nspan->_n -= k;_Spanlists[nspan->_n].push_front(nspan);return kspan;}}Span* maxspan = new Span;void* ptr = SystemAlloc(NPAGES - 1);maxspan->_pageId = (PAGEID)ptr >> PAGESHIFT;maxspan->_n = NPAGES - 1;_Spanlists[maxspan->_n].push_front(maxspan);return NewSpan(k);
}

  1. 首先,Page Cache 会尝试在对应的第 k 号桶中查找是否有可用的 span。如果有,则直接取出一个 span 返回给 Central Cache。

  2. 如果第 k 号桶中没有可用的 span,Page Cache 将继续在后面的桶中寻找。这时,Page Cache 不会立即向堆申请一个 k 页的 span,而是尝试找到一个大一点的 span,并根据需要将其切分成所需大小的 span。

  3. 当 Page Cache 在第 n+k 号桶中找到一个大小为 (n+k) 页的 span 时,它会将其切分成一个 n 页的 span 和一个 k 页的 span。然后,将 n 页的 span 挂到第 n 号桶中,而将 k 页的 span 返回给 Central Cache。

  4. 如果 Page Cache 在后续的桶中都找不到足够大的 span,那么就只能向堆申请一个较大的内存块,通常是 128 页大小的内存块。然后,Page Cache 将这个 128 页的 span 切分成一个 n 页的 span 和一个 (128-n) 页的 span,将 n 页的 span 返回给 Central Cache,而将 (128-n) 页的 span 挂到对应的桶中。

  5. 最后为了尽量避免出现重复的代码,我们最好不要再编写对应的切分代码。我们可以先将申请到的128页的span挂到page cache对应的哈希桶中,然后再递归调用该函数就行了,此时在往后找span时就一定会在第128号桶中找到该span,然后进行切分。

总的来说,Page Cache 会尽量以较大的内存块为单位进行分配,以便提高内存的连续性和管理效率。当需要获取较小的 span 时,Page Cache 会根据需要将大的 span 切分成合适大小的 span,而不是直接向堆申请小块大小的内存,以避免内存碎片化和管理复杂度的增加。

申请过程联调单元测试

当我们在C++代码中同时包含了algorithm头文件和Windows.h头文件时,可能会遇到名称冲突的问题。这是因为在Windows.h中定义了一个名为min的宏,与algorithm头文件中的min函数模板产生了冲突。由于编译器会优先匹配宏定义,因此当我们尝试调用std::min时,编译器会误认为我们想要调用Windows.h中的min宏,从而导致编译错误。这就是没有用命名空间进行封装的坏处,这时我们只能选择将std::去掉,让编译器调用Windows.h当中的min。                          

void TestAlloc()
{for (size_t i = 0; i < 1024; ++i){void* p1 = ConcurrentAlloc(6);cout << p1 << endl;}void* p2 = ConcurrentAlloc(8);cout << p2 << endl;
}int main()
{//TLSTest();TestAlloc();return 0;
}

这里我们删掉之前ConcurrentAlloc.h中用于测试ThreadCache的代码

	// cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;

这里我们选择一步一步调试观察程序的运行是否符合我们的预期

1)首先,在第一次的申请我们的_freelist都是空的会继续向CentralCache申请内存

2)之后在向CentralCache申请Span时也由于初始时为空,继而调用Newspan()向PageCache申请

 3)在Newspan中,在for循环外打上断点,再跳转到断点处

4)此时跳转成功,说明PageCache初始时为空,此时申请128页的大Span,符合预期

5)这里我们可以查看申请得到Span的内存地址和Span转化后的_pageId

  • ptr = 0x000001f579620000
  • _pageId  =  262916880

 通过计算我们可以验证_pageId左移PAGESHIFT实际上就可以得到对应的Span的地址

具体原因也是我们之前提到的我们调用的VirtualAlloc接口返回的内存地址实际上是会按照页为单位对齐的,所以我们在封装的时候就是以页为单位去申请内存,所以自然就可以做到这样的转化

6)然后,我们取消前一个断点,再向for循环内部的if内部打上断点

7)程序不仅成功进入断点而且把原先的128页的大Span拆分成了1页(返还给CentralCache)和127页(挂在Spanlist上)

 8)之后我们回到FetchFromCentralCache(),在判断取得一个内存块处打上断点,并且跳转成功,符合预期

9) 随后申请第一个内存块成功结束,成功打印出第一个结果

10) 接着,我们在for循环后打上断点,直接跳转观察1024此申请后的程序运行

这里我们做一下预估:1024次申请6字节,实际上由于内存对齐可以当作申请了1024次8字节,

一共就申请了8*1024 = 8192 = 8KB 也就是恰好一页的内存 , 从前面得知我们第一次向PageCache申请的恰好是放有一页的Span内存块,经过前面1024次申请的消耗完,本次申请将会在继续向PageCache申请新的Span

11) 这里再一次成功跳转到NewSpan的if内部,此时我们将上一次剩下的127页Span又一次拆分成了1页的Span返还再将126页挂回,符合预期

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

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

相关文章

【代码随想录 | 数组 01】二分查找

文章目录 1.二分查找1.1题目1.2思路&#xff08;核心&#xff1a;区间的定义&#xff09;1.3左闭右闭1.4左闭右开1.5总结 1.二分查找 1.1题目 704.二分查找—力扣题目链接 题目&#xff1a;给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 …

C语言分析基础排序算法——插入排序

目录 插入排序 直接插入排序 希尔排序 希尔排序基本思路解析 希尔排序优化思路解析 完整希尔排序文件 插入排序 直接插入排序 所谓直接插入排序&#xff0c;即每插入一个数据和之前的数据进行大小比较&#xff0c;如果较大放置在后面&#xff0c;较小放置在前面&#x…

LeetCode-102.题: 二叉树的层序遍历(原创)

【题目描述】 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]] 【题目链接…

【ICCV21】Swin Transformer: Hierarchical Vision Transformer using Shifted Windows

文章目录 0. Abstract1. Introduction2. Related Work3. Method3.1 Overall Architecture3.2 Shifted Window based Self-Attention3.3 Architecture Variants 4. Experiments4.1 Image Classification on ImageNet-1K4.2 Object Detection on COCO4.3 Semantic Segmentation o…

文本向量评测MTEB和C-MTEB

文章目录 简介MTEBC-MTEB参考资料 简介 MTEB(Massive Text Embedding Benchmark)是目前评测文本向量很重要的一个参考&#xff0c;其榜单也是各大文本向量模型用来展示与其他向量模型强弱的一个竞技台。 C-MTEB则是专门针对中文文本向量的评测基准。 MTEB MTEB的目的是为了…

OKLink2月安全月报| 2起典型漏洞攻击案例分析

在本月初我们发布的2024年2月安全月报中提到&#xff0c;2月全网累计造成损失约1.03亿美元。其中钓鱼诈骗事件损失占比11.76%。 OKLink提醒大家&#xff0c;在参与Web3项目时&#xff0c;应当仔细调研项目的真实性、可靠性&#xff0c;提升对钓鱼网站和风险项目的甄别能力&…

C语言从入门到熟悉------第二阶段

printf的用法 printf的格式有四种&#xff1a; &#xff08;1&#xff09;printf("字符串\n"); 其中\n表示换行的意思。其中n是“new line”的缩写&#xff0c;即“新的一行”。此外需要注意的是&#xff0c;printf中的双引号和后面的分号必须是在英文输入法下。双引…

portainer管理远程docker和docker-swarm集群

使用前请先安装docker和docker-compose&#xff0c;同时完成docker-swarm集群初始化 一、portainer-ce部署 部署portainer-ce实时管理本机docker&#xff0c;使用docker-compose一键拉起 docker-compose.yml version: 3 services:portainer:container_name: portainer#imag…

[机器视觉]halcon应用实例 找圆

[机器视觉]halcon应用实例 找圆 代码 *清空屏幕&#xff0c;显示控制图像 dev_close_window () dev_update_off () read_image (Image, 形状模板图) dev_open_window_fit_image (Image, 0, 0, -1, -1, WindowHandle) dev_display (Image) *创建测量模型 create_metrology_mod…

AD20新建工程步骤

1 新建工程 2 创建 3 新建原理图 4 新建PCB图 5 对原理图贺PCB都进行保存 6 新建原理图库贺PCB库&#xff0c;以及保存 最后在保存位置上都可以看到 打开的时候直接打开工程&#xff0c;它自己就会把这些链接在一起

UVa11595 Crossing Streets EXTREME

题目链接 UVa11595 - Crossing Streets EXTREME 题意 平面上有 n&#xff08;n≤35&#xff09;条直线&#xff0c;各代表一条街道。街道相互交叉&#xff0c;形成一些路段&#xff08;对应于几何上的线段&#xff09;。你的任务是设计一条从A到B的路线&#xff0c;使得穿过路…

c++: 引用能否替代指针? 详解引用与指针的区别.

文章目录 前言1. 引用和指针的最大区别:引用不能改变指向2. 引用和指针在底层上面是一样的3. 引用和指针在sizeof面前大小不同4. 有多级指针,没有多级引用5.引用是引用的实体,指针会向后偏移同一个类型的大小 总结 前言 新来的小伙伴如果不知道引用是什么?可以看我的上一篇文…

AI新工具(20240312) Midjourney官方发布角色一致性功能;免费且开源的简历制作工具;精确克隆语调、控制声音风格

1: Midjourney角色一致性功能 使人物画像在多方面高度一致成为可能。 Midjourney的角色一致性功能的使用方法如下&#xff1a; ⭐在你的输入指令后面加上 --cref URL&#xff0c;其中URL是你选择的角色图像的链接。 ⭐你可以通过 --cw 参数来调整参照的强度&#xff0c;范围…

spring boot 使用 webservice

spring boot 使用 webservice 使用 java 自带的 jax-ws 依赖 如果是jdk1.8,不需要引入任何依赖&#xff0c;如果大于1.8 <dependency><groupId>javax.jws</groupId><artifactId>javax.jws-api</artifactId><version>1.1</version&g…

【Linux】文件缓冲区|理解文件系统

目录 预备知识 观察现象 第一&#xff1a;携带\n&#xff0c;不使用fork()&#xff0c;打印到显示器 第二&#xff1a;携带\n&#xff0c;使用fork()&#xff0c;打印到显示器 第三&#xff1a;携带\n&#xff0c;使用fork()&#xff0c;打印到文件里 第四&#xff1a;不携…

案例分析篇03:一篇文章搞定软考设计模式考点(2024年软考高级系统架构设计师冲刺知识点总结系列文章)

专栏系列文章推荐: 2024高级系统架构设计师备考资料(高频考点&真题&经验)https://blog.csdn.net/seeker1994/category_12593400.html 【历年案例分析真题考点汇总】与【专栏文章案例分析高频考点目录】(2024年软考高级系统架构设计师冲刺知识点总结-案例分析篇-…

IPSec NAT穿越原理

一、IPSec VPN在NAT场景中存在的问题 当某些组网中&#xff0c;有的分支连动态的公网IP地址也没有&#xff0c;只能由网络中的NAT设备进行地址转换&#xff0c;才能访问互联网&#xff0c;然而IPsec是用来保护报文不被修改的&#xff0c;而NAT需要修改报文的IP地址&#xff0c…

十大排序算法(冒泡排序、插入排序、选择排序、希尔排序、堆排序、快排、归并排序、桶排序、计数排序、基数排序)

目录 一、冒泡排序&#xff1a; 二、插入排序&#xff1a; 三、选择排序&#xff1a; 四、希尔排序&#xff1a; 五、堆排序&#xff1a; 六、快速排序&#xff1a; 6.1挖坑法&#xff1a; 6.2左右指针法 6.3前后指针法&#xff1a; 七、归并排序&#xff1a; 八、桶…

力扣hot100:240.搜索二维矩阵II(脑子)

吉大21级算法分析与设计的一道大题&#xff0c;由于每一行都是排好序的直接逐行二分 可以达到&#xff1a;O(mlogn)。但是这里追求更广的思路可以使用其他方法。 矩阵四分&#xff1a; 在矩阵中用中心点比较&#xff0c;如果target大于中心点的值&#xff0c;则由于升序排列&am…

vue学习笔记23-组件事件⭐

组件事件 在组件的模板表达式中&#xff0c;可以直接使用$emit方法触发自定义事件&#xff1b;触发自定义事件的目的是组件之间传递数据 好好好今天又碰到问题了&#xff0c;来吧来吧 测试发现其他项目都可以 正常的run ,就它不行 搜索发现新建项目并进入以后&#xff0c;用指…