protobuf arena实现概述

article/2025/6/7 15:33:27

 Arena是Protobuf的C++特有特性,旨在优化内存分配效率,减少频繁的堆内存申请与释放。其核心机制如下:

  • 预分配内存:Arena预先分配一大块连续内存(称为Block),对象创建时直接从该内存块中分配,避免了频繁调用new/malloc
  • 批量释放:当Arena生命周期结束时,所有分配的内存一次性释放,无需逐个调用析构函数
  • 内存复用:内存块不足时,按倍增策略(如初始4KB,最大64KB)扩展新块,减少内存碎片并提升缓存命中率

Arena的优点:

  1. 性能提升:减少内存分配次数,尤其适合创建大量短生命周期对象(如解析消息、序列化)。连续内存布局提高缓存命中率,加速数据编译
  2. 内存管理优化:批量释放内存,避免内存泄漏风险。支持跳过析构函数,避免不必要的析构调用
  3. 线程安全:Arena的分配操作线程安全,但销毁需由单一线程控制

下面通过一个简单的例子大致梳理Arena的源码实现。首先定义一个proto文件person.proto:

syntax = "proto3";
option cc_enable_arenas = true;package tutorial;message Person {int32 id = 1;repeated int32 value = 2;string name = 3;
}

上述proto文件大致涵盖了几个常用的数据类型,包括基本数据类型、repeated及string数据类型。基于该proto文件使用protoc生成关于类Person的C++代码。使用Arena构造数据对象的code如下:


int32_t counter = 0;while (!hasTerminationRequested()){google::protobuf::Arena arena(options);tutorial::Person *person = google::protobuf::Arena::CreateMessage<tutorial::Person>(&arena);person->set_id(counter++);person->add_value(counter);person->add_value(counter + 1);person->add_value(counter + 2);person->set_name("helloworld" + std::to_string(counter));std::this_thread::sleep_for(CYCLE_TIME);}

Arena负责建立和管理内存池,可以通过Arena对象申请内存池中的内存,在该内存上构造新的protobuf message对象。那么内存池的初始内存是怎么分配的呢?先放一张Arena对象整体管理的内存相关的数据结构示意图:

每个线程通过SerialArena对象管理一个block块内存区链表,当前线程的message对象的内存优先从该block链表中分配。SerialArena对象在第一个初始化的block内存区中构造。各个线程的SerialArena通过链表关联起来,由Arena对象管理。

下面来看特定线程中第一个block的初始化过程:

void ThreadSafeArena::InitializeWithPolicy(void* mem, size_t size,AllocationPolicy policy) {constexpr size_t kAPSize = internal::AlignUpTo8(sizeof(AllocationPolicy));constexpr size_t kMinimumSize = kBlockHeaderSize + kSerialArenaSize + kAPSize; (1)if (mem != nullptr && size >= kMinimumSize) {alloc_policy_.set_is_user_owned_initial_block(true);} else {auto tmp = AllocateMemory(&policy, 0, kMinimumSize);                       (2)mem = tmp.ptr;size = tmp.size;}
}

(1)第一个Block的大小至少能够容纳Block、SerialArena及AllocationPolicy三个对象的大小。AllocationPolicy控制内存分配的方式,扩容策略等。

(2)实际分配内存

AllocateMemory的实现:

static SerialArena::Memory AllocateMemory(const AllocationPolicy* policy_ptr,size_t last_size, size_t min_bytes) {AllocationPolicy policy;  // default policyif (policy_ptr) policy = *policy_ptr;size_t size;if (last_size != 0) {// Double the current block size, up to a limit.auto max_size = policy.max_block_size;size = std::min(2 * last_size, max_size);    (1)} else {size = policy.start_block_size;              (2)}// Verify that min_bytes + kBlockHeaderSize won't overflow.GOOGLE_CHECK_LE(min_bytes,std::numeric_limits<size_t>::max() - SerialArena::kBlockHeaderSize);size = std::max(size, SerialArena::kBlockHeaderSize + min_bytes);void* mem;if (policy.block_alloc == nullptr) {mem = ::operator new(size);                  (3)} else {mem = policy.block_alloc(size, size);        (4)}return {mem, size};
}

(1)(2)表明如果是第一个Block,大小为start_block_size(内存分配策略AllocationPolicy中的数据成员),之后如果继续分配Block,其大小为前一个大小的两倍,但是存在上限max_block_size(同样为内存分配策略AllocationPolicy中的数据成员)

(3)(4)表明内存分配方式可以由用户自定义,也可以使用默认的new分配方式从堆上分配

内存分配好,要在上面构造Block结构体了:

void ThreadSafeArena::InitializeWithPolicy(void* mem, size_t size,AllocationPolicy policy) {SetInitialBlock(mem, size);
}void ThreadSafeArena::SetInitialBlock(void* mem, size_t size) {SerialArena* serial = SerialArena::New({mem, size}, &thread_cache());serial->set_next(NULL);threads_.store(serial, std::memory_order_relaxed);CacheSerialArena(serial);
}SerialArena* SerialArena::New(Memory mem, void* owner) {GOOGLE_DCHECK_LE(kBlockHeaderSize + ThreadSafeArena::kSerialArenaSize, mem.size);auto b = new (mem.ptr) Block{nullptr, mem.size};                 (1)return new (b->Pointer(kBlockHeaderSize)) SerialArena(b, owner); (2)
}

先从分配内存的起始处构造Block对象,紧接着Block对象构造SerialArena对象。SerialArena对象的构造示意如下:

limit_为什么要向下对齐到8字节呢?因为从这个位置开始存储对象的析构函数地址。

接着看InitializeWithPolicy()的实现:

void ThreadSafeArena::InitializeWithPolicy(void* mem, size_t size,AllocationPolicy policy) {void* p;if (!sa || !sa->MaybeAllocateAligned(kAPSize, &p)) {GOOGLE_LOG(FATAL) << "MaybeAllocateAligned cannot fail here.";return;}new (p) AllocationPolicy{policy};
}

紧跟着SerialArena对象构造AllocationPolicy对象,示意如下:

假设现在要通过Arena对象分配一段内存,并在该内存上构造一个新的message对象:

 google::protobuf::Arena arena(options);tutorial::Person *person = google::protobuf::Arena::CreateMessage<tutorial::Person>(&arena);

看下CreateMessage的实现:

template <typename T, typename... Args>
PROTOBUF_ALWAYS_INLINE static T* CreateMessage(Arena* arena, Args&&... args) {static_assert(InternalHelper<T>::is_arena_constructable::value,"CreateMessage can only construct types that are ArenaConstructable");// We must delegate to CreateMaybeMessage() and NOT CreateMessageInternal()// because protobuf generated classes specialize CreateMaybeMessage() and we// need to use that specialization for code size reasons.return Arena::CreateMaybeMessage<T>(arena, static_cast<Args&&>(args)...);
}PROTOBUF_NAMESPACE_OPEN
template<> PROTOBUF_NOINLINE ::tutorial::Person* Arena::CreateMaybeMessage< ::tutorial::Person >(Arena* arena) {return Arena::CreateMessageInternal< ::tutorial::Person >(arena);
}template <typename T, typename... Args>
PROTOBUF_NDEBUG_INLINE static T* CreateMessageInternal(Arena* arena,Args&&... args) {static_assert(InternalHelper<T>::is_arena_constructable::value,"CreateMessage can only construct types that are ArenaConstructable");if (arena == NULL) {return new T(nullptr, static_cast<Args&&>(args)...);} else {return arena->DoCreateMessage<T>(static_cast<Args&&>(args)...);}
}

最终调用DoCreateMessage:

template <typename T, typename... Args>
PROTOBUF_NDEBUG_INLINE T* DoCreateMessage(Args&&... args) {return InternalHelper<T>::Construct(AllocateInternal(sizeof(T), alignof(T),internal::ObjectDestructor<InternalHelper<T>::is_destructor_skippable::value,T>::destructor,RTTI_TYPE_ID(T)),this, std::forward<Args>(args)...);
}

以上实现可以概括为利用AllocateInternal分配内存,在分配的内存上构造message对象。AllocateInternal会调用AllocateAlignedWithCleanup:

std::pair<void*, SerialArena::CleanupNode*>
ThreadSafeArena::AllocateAlignedWithCleanup(size_t n,const std::type_info* type) {SerialArena* arena;if (PROTOBUF_PREDICT_TRUE(!alloc_policy_.should_record_allocs() &&GetSerialArenaFast(&arena))) {return arena->AllocateAlignedWithCleanup(n, alloc_policy_.get());} else {return AllocateAlignedWithCleanupFallback(n, type);}
}std::pair<void*, CleanupNode*> AllocateAlignedWithCleanup(size_t n, const AllocationPolicy* policy) {GOOGLE_DCHECK_EQ(internal::AlignUpTo8(n), n);  // Must be already aligned.if (PROTOBUF_PREDICT_FALSE(!HasSpace(n + kCleanupSize))) {   (1)return AllocateAlignedWithCleanupFallback(n, policy);}return AllocateFromExistingWithCleanupFallback(n);           (2)}

(1)中kCleanupSize = AlignUpTo8(sizeof(CleanupNode)),CleanupNode的定义为:

struct CleanupNode {void* elem;              // Pointer to the object to be cleaned up.void (*cleanup)(void*);  // Function pointer to the destructor or deleter.
};

CleanupNode对象存储message对象的指针和其析构函数地址,在资源清理时会用到,该对象的构造从上面提到的limit_处开始。

(2)暂时只考虑第一次分配的Block对象空间大小满足需求:

std::pair<void*, CleanupNode*> AllocateFromExistingWithCleanupFallback(size_t n) {void* ret = ptr_;ptr_ += n;limit_ -= kCleanupSize;
#ifdef ADDRESS_SANITIZERASAN_UNPOISON_MEMORY_REGION(ret, n);ASAN_UNPOISON_MEMORY_REGION(limit_, kCleanupSize);
#endif  // ADDRESS_SANITIZERreturn CreatePair(ret, reinterpret_cast<CleanupNode*>(limit_));}

内存的分配仅仅是prt_及limit_位置的移动。继续看AllocateInternal的实现:

PROTOBUF_NDEBUG_INLINE void* AllocateInternal(size_t size, size_t align,void (*destructor)(void*),const std::type_info* type) {// Monitor allocation if needed.if (destructor == nullptr) {return AllocateAlignedWithHook(size, align, type);} else {if (align <= 8) {auto res = AllocateAlignedWithCleanup(internal::AlignUpTo8(size), type);res.second->elem = res.first;res.second->cleanup = destructor;return res.first;} else {auto res = AllocateAlignedWithCleanup(size + align - 8, type);auto ptr = internal::AlignTo(res.first, align);res.second->elem = ptr;res.second->cleanup = destructor;return ptr;}}}

该函数结束后内存布局如下(假设构造的对象为t1):

继续分配新的message对象(假设构造的对象为t2),分两种情况:

1. 若第一个Block空间仍然足够:

2.若第一个Block空间不足,无法继续容纳t2:

PROTOBUF_NOINLINE
std::pair<void*, SerialArena::CleanupNode*>
SerialArena::AllocateAlignedWithCleanupFallback(size_t n, const AllocationPolicy* policy) {AllocateNewBlock(n + kCleanupSize, policy);return AllocateFromExistingWithCleanupFallback(n);
}

先分配一个新的Block,再在新的Block空间上为message对象分配空间。

void SerialArena::AllocateNewBlock(size_t n, const AllocationPolicy* policy) {// Sync limit to blockhead_->start = reinterpret_cast<CleanupNode*>(limit_);   (1)// Record how much used in this block.space_used_ += ptr_ - head_->Pointer(kBlockHeaderSize);auto mem = AllocateMemory(policy, head_->size, n);// We don't want to emit an expensive RMW instruction that requires// exclusive access to a cacheline. Hence we write it in terms of a// regular add.auto relaxed = std::memory_order_relaxed;space_allocated_.store(space_allocated_.load(relaxed) + mem.size, relaxed);head_ = new (mem.ptr) Block{head_, mem.size};ptr_ = head_->Pointer(kBlockHeaderSize);limit_ = head_->Pointer(head_->size);
}

(1)将CleanUp对象的起始地址缓存在同一个block上的Block对象的start数据成员,资源清理的适合会遍历调用:

void SerialArena::CleanupList() {Block* b = head_;b->start = reinterpret_cast<CleanupNode*>(limit_);do {auto* limit = reinterpret_cast<CleanupNode*>(b->Pointer(b->size & static_cast<size_t>(-8)));auto it = b->start;auto num = limit - it;if (num > 0) {for (; it < limit; it++) {it->cleanup(it->elem);}}b = b->next;} while (b);
}

AllocateNewBlock执行完成后,ptr_和limit_会指向新分配的block上的地址:

以上是固定大小message内存分配和构造的过程,接下来分析repeated字段存取操作实现:

google::protobuf::Arena arena(options);
tutorial::Person *person = google::protobuf::Arena::CreateMessage<tutorial::Person>(&arena);person->add_value(counter);
person->add_value(counter + 1);
person->add_value(counter + 2);

repeated字段对应生成的c++代码中的RepeatedField,构造函数为:

template <typename Element>
inline RepeatedField<Element>::RepeatedField(Arena* arena): current_size_(0), total_size_(0), arena_or_elements_(arena){}class RepeatedField final {
private:void* arena_or_elements_;
};

传入的Arena对象的指针存储在arena_or_elements_。add_value的实现:

inline void Person::add_value(int32_t value) {_internal_add_value(value);// @@protoc_insertion_point(field_add:tutorial.Person.value)
}inline void Person::_internal_add_value(int32_t value) {value_.Add(value);
}template <typename Element>
inline void RepeatedField<Element>::Add(const Element& value) {uint32_t size = current_size_;if (static_cast<int>(size) == total_size_) {// value could reference an element of the array. Reserving new space will// invalidate the reference. So we must make a copy first.auto tmp = value;Reserve(total_size_ + 1);elements()[size] = std::move(tmp);} else {elements()[size] = value;}current_size_ = size + 1;
}

Reserve的实现的核心是struct Rep对象的构造及arena_or_elements_的赋值:

template <typename Element>
void RepeatedField<Element>::Reserve(int new_size) {if (total_size_ >= new_size) return;Rep* old_rep = total_size_ > 0 ? rep() : nullptr;Rep* new_rep;Arena* arena = GetArena();new_size = internal::CalculateReserveSize(total_size_, new_size);GOOGLE_DCHECK_LE(static_cast<size_t>(new_size),(std::numeric_limits<size_t>::max() - kRepHeaderSize) / sizeof(Element))<< "Requested size is too large to fit into size_t.";size_t bytes =kRepHeaderSize + sizeof(Element) * static_cast<size_t>(new_size);if (arena == nullptr) {new_rep = static_cast<Rep*>(::operator new(bytes));} else {new_rep = reinterpret_cast<Rep*>(Arena::CreateArray<char>(arena, bytes));}new_rep->arena = arena;int old_total_size = total_size_;// Already known: new_size >= internal::kMinRepeatedFieldAllocationSize// Maintain invariant://     total_size_ == 0 ||//     total_size_ >= internal::kMinRepeatedFieldAllocationSizetotal_size_ = new_size;arena_or_elements_ = new_rep->elements;// Invoke placement-new on newly allocated elements. We shouldn't have to do// this, since Element is supposed to be POD, but a previous version of this// code allocated storage with "new Element[size]" and some code uses// RepeatedField with non-POD types, relying on constructor invocation. If// Element has a trivial constructor (e.g., int32_t), gcc (tested with -O2)// completely removes this loop because the loop body is empty, so this has no// effect unless its side-effects are required for correctness.// Note that we do this before MoveArray() below because Element's copy// assignment implementation will want an initialized instance first.Element* e = &elements()[0];Element* limit = e + total_size_;for (; e < limit; e++) {new (e) Element;}if (current_size_ > 0) {MoveArray(&elements()[0], old_rep->elements, current_size_);}// Likewise, we need to invoke destructors on the old array.InternalDeallocate(old_rep, old_total_size);}

RepeatedField本质上利用数组实现,其内存空间的获取跟前述固定大小message对象的内存空间获取类似。现在假设已经为RepeatedField分配了一段内存,其内存布局如下:

若在设置值的过程中,内存大小已经不满足repeated字段值的继续添加,会做以下三件事情:

1. 在新的内存上构造message对象:

Element* e = &elements()[0];
Element* limit = e + total_size_;
for (; e < limit; e++) {new (e) Element;
}

2. 将已经设置的repeated字段值拷贝到新分配的内存:

if (current_size_ > 0) {MoveArray(&elements()[0], old_rep->elements, current_size_);
}

3. 析构在旧的内存上构造的对象:

void InternalDeallocate(Rep* rep, int size) {if (rep != nullptr) {Element* e = &rep->elements[0];if (!std::is_trivial<Element>::value) {Element* limit = &rep->elements[size];for (; e < limit; e++) {e->~Element();}}if (rep->arena == nullptr) {
#if defined(__GXX_DELETE_WITH_SIZE__) || defined(__cpp_sized_deallocation)const size_t bytes = size * sizeof(*e) + kRepHeaderSize;::operator delete(static_cast<void*>(rep), bytes);
#else::operator delete(static_cast<void*>(rep));
#endif}}}

repeated字段的获取:

for (int i = 0; i < person->value_size(); ++i) {std::cout << person->value(i) << " ";
}inline const ::PROTOBUF_NAMESPACE_ID::RepeatedField< int32_t >&
Person::value() const {// @@protoc_insertion_point(field_list:tutorial.Person.value)return _internal_value();
}template <typename Element>
inline const Element& RepeatedField<Element>::Get(int index) const {GOOGLE_DCHECK_GE(index, 0);GOOGLE_DCHECK_LT(index, current_size_);return elements()[index];
}

从以上代码可以看出,数据的获取依赖arena_or_elements_,该值被设为数组的起始地址。

最后看看string字段的存取:

google::protobuf::Arena arena(options);
tutorial::Person *person = google::protobuf::Arena::CreateMessage<tutorial::Person>(&arena);person->set_name("zerocopy" + std::to_string(counter));void ArenaStringPtr::Set(const std::string* default_value, std::string&& value,::google::protobuf::Arena* arena) {if (IsDefault(default_value)) {if (arena == nullptr) {tagged_ptr_.Set(new std::string(std::move(value)));} else {tagged_ptr_.Set(Arena::Create<std::string>(arena, std::move(value)));}} else if (IsDonatedString()) {std::string* current = tagged_ptr_.Get();auto* s = new (current) std::string(std::move(value));arena->OwnDestructor(s);tagged_ptr_.Set(s);} else /* !IsDonatedString() */ {*UnsafeMutablePointer() = std::move(value);}
}

也是借助Arena对象申请对象,然后在内存上构造string对象。

最后提出一个问题供大家思考,基于protobuf能否实现共享内存上的零拷贝?


http://www.hkcw.cn/article/txjfiAplbl.shtml

相关文章

深入浅出图神经网络:从核心概念到实战落地

文章目录 1 引言1.1 发展脉络与现状1.2 面临挑战1.3 本文目标 2 图结构数据基础2.1 关键元素2.2 数学定义与常用符号2.3 图的常见类型2.4 为什么这些定义重要&#xff1f; 3 GNN 核心思想&#xff1a;消息传递机制3.1 消息函数 M E S S A G E ( k ) \mathrm{MESSAGE}^{(k)} ME…

6级阅读学习

先找连接词&#xff0c;and什么的 再找that什么的 最后找介词短语

当 AI 超越人类:从技术突破到文明拐点的 2025-2030 年全景展望

引言:当科幻照进现实的十年 2025 年的某个清晨,当你对着智能音箱说出 “帮我订一份早餐” 时,或许不会想到,这个简单指令背后的技术演进,正悄然推动人类文明走向一个前所未有的拐点。从弱人工智能(ANI)到强人工智能(AGI)的跃迁,不再是科幻小说的专属设定,而是现实世…

安全-JAVA开发-第一天

目标&#xff1a; 安装环境 了解基础架构 了解代码执行顺序 与数据库进行连接 准备&#xff1a; 安装 下载IDEA并下载tomcat&#xff08;后续出教程&#xff09; 之后新建项目 注意点如下 1.应用程序服务器选择Web开发 2.新建Tomcat的服务器配置文件 并使用 Hello…

Spring @Autowired自动装配的实现机制

Spring Autowired自动装配的实现机制 Autowired 注解实现原理详解一、Autowired 注解定义二、Qualifier 注解辅助指定 Bean 名称三、BeanFactory&#xff1a;按类型获取 Bean四、注入逻辑实现五、小结 源码见&#xff1a;mini-spring Autowired 注解实现原理详解 Autowired 的…

【AI News | 20250603】每日AI进展

AI Repos 1、dgm 是一个创新的自改进系统&#xff0c;通过迭代修改自身代码并利用编码基准验证每次更改&#xff0c;实现开放式进化。该系统旨在提升 AI 代理的代码修改能力。DGM 支持 OpenAI 和 Anthropic API&#xff0c;依赖 Docker 环境&#xff0c;并集成了 SWE-bench 和…

Rust 学习笔记:Cargo 工作区

Rust 学习笔记&#xff1a;Cargo 工作区 Rust 学习笔记&#xff1a;Cargo 工作区创建工作区在工作区中创建第二个包依赖于工作区中的外部包向工作区添加测试将工作区中的 crate 发布到 crates.io添加 add_two crate 到工作区总结 Rust 学习笔记&#xff1a;Cargo 工作区 随着项…

操作系统 第 39 章 插叙:文件和目录

两项关键操作系统技术的发展&#xff1a;进程&#xff0c;虚拟化的 CPU&#xff1b;地址空间&#xff0c;虚拟化的内存。 这一部分加上虚拟化拼图中最关键的一块&#xff1a;持久存储。永久存储设备永久地&#xff08;或至少长时间地&#xff09;存储信息&#xff0c;如传统硬盘…

楼宇自控系统联动暖通空调:解密建筑环境舒适度提升路径

走进现代建筑&#xff0c;无论是办公场所、商业中心&#xff0c;还是医院、酒店&#xff0c;人们对环境舒适度的要求越来越高。暖通空调作为调节建筑室内环境的关键设备&#xff0c;其运行效果直接影响着人们的体验。然而&#xff0c;传统暖通空调独立运行、调控不灵活等问题&a…

Freemarker快速入门

Freemarker概述 FreeMarker 是一款 模板引擎&#xff1a; 即一种基于模板和要改变的数据&#xff0c; 并用来生成输出文本(HTML网页&#xff0c;电子邮件&#xff0c;配置文件&#xff0c;源代码等)的通用工具。 它不是面向最终用户的&#xff0c;而是一个Java类库&#xff0c…

黑盒(功能)测试基本方法

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、黑盒测试的概念 1、什么是黑盒测试 &#xff08;1&#xff09;黑盒测试又称功能测试、数据驱动测试或基于规格说明书的测试&#xff0c;是一种从用户观点出…

[java八股文][JavaSpring面试篇]SpringCloud

了解SpringCloud吗&#xff0c;说一下他和SpringBoot的区别 Spring Boot是用于构建单个Spring应用的框架&#xff0c;而Spring Cloud则是用于构建分布式系统中的微服务架构的工具&#xff0c;Spring Cloud提供了服务注册与发现、负载均衡、断路器、网关等功能。 两者可以结合…

chromedriver 下载失败

问题描述 chromedriver 2.46.0 下载失败 淘宝https://registry.npmmirror.com/chromedriver/2.46/chromedriver_win32.zip无法下载 解决方法 找到可下载源 https://cdn.npmmirror.com/binaries/chromedriver/2.46/chromedriver_win32.zip &#xff0c;先将其下载到本地目录(D…

74. 搜索二维矩阵 (力扣)

给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target 在矩阵中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。…

CppCon 2014 学习:Rolling Your Own Circuit Simulator

这段话讲述了一个背景和动机&#xff0c;目的是阐明为什么开源C库变得越来越复杂且在科学和工程领域有很大的应用潜力。 关键点&#xff1a; 开源库的成熟&#xff1a; 近年来&#xff0c;开源C库在许多科学和工程领域变得越来越成熟和强大。这些库不再仅仅是简单的工具&…

无人机自主降落论文解析

Dynamic Landing of an Autonomous Quadrotor on a Moving Platform in Turbulent Wind Conditions 滑膜控制器 这一部分详细介绍了边界层滑模控制器&#xff08;Boundary Layer Sliding Controller&#xff0c;BLSC&#xff09;的设计和实现&#xff0c;特别是如何将其应用于…

.NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想

在当今数据驱动的时代&#xff0c;向量数据库&#xff08;Vector Database&#xff09;作为一种新兴的数据库技术&#xff0c;正逐渐成为软件开发领域的重要组成部分。特别是在 .NET 生态系统中&#xff0c;向量数据库的应用为开发者提供了构建智能、高效应用程序的新途径。 一…

html基础01:前端基础知识学习

html基础01&#xff1a;前端基础知识学习 1.个人建立打造 -- 之前知识的小总结1.1个人简历展示1.2简历信息填写页面 1.个人建立打造 – 之前知识的小总结 1.1个人简历展示 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8&qu…

CentOS Stream 8 Unit network.service not found

一、问题现象 在 CentOS Stream 8 操作系统中&#xff0c;配置完静态IP 信息&#xff0c;想重启网络服务。 执行如下命令&#xff1a; systemctl restart network 提示信息如下&#xff1a; Failed to restart network.service: Unit network.service not found. 二、问题…

【Axure高保真原型】交通事故大屏可视化分析案例

今天和大家分享交通事故大屏可视化分析案例的原型模板&#xff0c;包括饼图分类分析、动态显示发生数、柱状图趋势分析、中部地图展示最新事故发现地点和其他信息、右侧列表记录发生事故的信息…… 通过多种可视化图表展示分析结果&#xff0c;具体效果可以点击下方视频观看或…