jemalloc 5.3.0的base模块的源码及调用链使用场景的详细分析

一、背景

这篇博客,我们继续之前的 由jemalloc 5.3.0初始化时的内存分配的分析引入jemalloc的三个关键概念及可借鉴的高性能编码技巧-CSDN博客 博客里对初始化分配逻辑进行分析,已经涉及到了jemalloc 5.3.0里的非常重要的base模块的一部分逻辑,在这篇博客里,我们进一步展开分析base模块,针对的场景也依然是初始化分配逻辑这块来作为切入口。

在第二章里,我们先顺着之前的博客 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 里 3.2.4 里重点提到的malloc_init_hard函数,继续展开分析,在 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 博客里,我们分析了初始化逻辑里的tsd模块的初始化逻辑,在 由jemalloc 5.3.0初始化时的内存分配的分析引入jemalloc的三个关键概念及可借鉴的高性能编码技巧-CSDN博客 博客里,我们讲到了几个关键概念group、delta、class还有sz.h里几个常用的psz,size,index之间的转换函数及相关含义,有了这两篇的基础,base模块的详细分析相对容易一些。

我们在第二章,我们依然以malloc_init_hard里内存分配逻辑作为切入口,来展开详细分析base模块,对于相关联的概念也会一一进行介绍,在第三章里,我们会用思维导图来汇总一下并附上总结说明。

二、依然以malloc_init_hard里的内存分配逻辑作为切入口,展开详细分析base模块

2.1 base_boot里的2M的分配用的是base_block_alloc函数,是base模块的分配函数

jemalloc 5.3.0的第一次内存分配的地方就是这个malloc_init_hard_a0_locked里的base_boot函数最终调用的base_map进行的2M的分配。

这块我们在之前的博客 由jemalloc 5.3.0初始化时的内存分配的分析引入jemalloc的三个关键概念及可借鉴的高性能编码技巧-CSDN博客 里已经做了一些介绍,这篇博客里需要再展开分析一下。

base_boot有关的调用链是:

malloc_init_hard->malloc_init_hard_a0_locked->base_boot->base_new->base_block_alloc->base_map 

我们先说一下base模块是什么?用来干什么?

2.2 base模块是一个metadata数据的一个分配器

base模块的文件就两个base.h和base.c。

base模块是一个metadata数据的一个分配器。那么什么是metadata数据呢?metadata就是元数据,也就是数据的数据。

base模块只分配元数据的内存,而并不分配malloc的客户所要的内存,也就是说,malloc的使用者所要的内存是由jemalloc里的base分配器以外的其他的底层内存分配器来分配的,这一点我们下面会用堆栈截图来证明。

2.2.1 三个用于分配的base接口都可能会调用到base_block_alloc函数

base模块用于分配的主要接口是三个,base_new函数和base_allocbase_alloc_edata,其中base_allocbase_alloc_edata函数,其中base_new用于base的初始化(base本身也是一个元数据),另外两个,则是在指定的base里分配元数据,当然,虽然说在指定的base里分配,但是还是可能按需扩容的,并不是说base_new分配出内存了以后,base_allocbase_alloc_edata就不会分配新的内存了(关于非base分配器来分配出来的调用链例子在下面 2.4 一节里会讲到)。这三个函数在需要分配新的内存时都会调用base_block_alloc函数,这个函数我们会在后面详细展开描述。

下图是base_new函数调用base_block_alloc函数的截图:

base_allocbase_alloc_edata,这两个函数都会调用base_alloc_impl:

而base_alloc_impl会在判断出当前空间不够时调用base_extent_alloc进行更多内存的分配:

而base_extent_alloc就会调用base_block_alloc进行内存分配:

2.2.2 分配元数据的base_alloc_edata接口相比base_alloc接口需要在base_alloc_impl时取回sn号,设到base_alloc_edata接口返回的是edata_t指针里去

base_allocbase_alloc_edata两个函数的主要区别在于base_alloc_edata需要在base_alloc_impl时取回sn号,也就是序列号,并设到新创建的edata_t这个元数据里。

这个sn号是base实例管理的,在base结构初始化时设置成0:

在base_new里创建完元数据后,把上图里的extent_sn_next记录到了base实例里:

base_new里调用base_block_alloc创建元数据时会把传入的表示sn号的指针指向的值加1:

base_block_alloc里调用了base_edata_init函数:

在base_edata_init函数里进行了+1:

你可能会问,这个多出来的sn号有什么用,在下面的 2.7.1 里会讲到。

2.3 base模块分配了哪些元数据?

我们列一下base模块分配的元数据的种类,列出的都是相对重要的元数据,用一次调用栈例子来说明(当然分配的同一种元数据对应的调用栈也可能是不一样的,我们只是举其中的一次调用栈来说明)。

2.3.1 base模块首先会分配base_t实例,也就是base自己这个元数据

上图的堆栈是main之前的第一次malloc的调用,这次malloc的调用我们在之前的博客 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 里也说明了是因为preload的jemalloc库时由于jemalloc的实现里包含了C++的内容,所以需要在main之前的初始化流程里做相关的C++的异常处理用的pool的分配。而上图中的堆栈,是由这一次分配触发,判断出整个jemalloc还未进行初始化,所以调用了malloc_init_hard接口(关于malloc_init_hard接口我们之前的博客里 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 的 3.2.4 分析过一部分),这个函数会间接调用base_boot来初始化第一个base实例,base_boot继而调用了base_new,base_new继而调用了base_block_alloc进行了分配,base_block_alloc使用base_map,base_map调用pages_map,注意,pages_map是base分配器以外的其他jemalloc内存分配器都会用到的一个接口,定义在src/pages.c里,它并不属于base模块

看base_boot的返回类型就可以体现它分配的就是base_t这个元数据实例:

0是第一个base,第一个base由一个src/base.c里的上截图的static base_t *b0变量来保存的。

2.3.2 base模块会分配提供静态的tcache的tbins信息的cache_bin_info_t数组

cache_bin_info_t数组的首地址定义如下:

它需要动态分配因为其大小不可静态确定。

相关的核心调用代码截图:

可以看到上图里的这次分配大小不大,就82字节,n_reserved_bins是41,41来自于nhbins,因为nhbins比SC_NBINS的值大(SC_NBINS是36):

相关的上下文调用链如下:

2.3.3 base模块会分配管理arena的arena_t的内存

arena是一个内存分配区,不同的线程一般属于不同的arena,默认情况下arena的个数是cpu核心数*4。

base模块分配arena实例的代码逻辑如下:

如上图可以看到一个关于arena实例分配的细节:

arena_t的大小是一个动态值,因为arena_t用了柔性数组:

相关base分配arena的调用链截图:

要注意,这次main之前的这个arena的分配由于分配的空间不大,用之前base分配出来的内存池子里的剩余空间就足够了,所以这次分配并没有触发base_map及底下分配接口。关于从base里挑选可用的空间来进行分配的逻辑,见 2.7 一节。

2.3.4 base模块会分配用来管理实际使用数据内存块的edata_t管理结构

先说明一下edata,它是管理的实际使用的数据内存块,这么说,edata还是一个元数据的数据结构,它管理对应的内存分为两种,一种是用户使用malloc接口触发进行分配的内存,另一种是jemalloc实现的内部逻辑调用泛iallocztm或泛ipallocztm的接口进行内部使用的内存分配

jemalloc对于不同大小的内存会有不同的策略,对于小size的class,jemalloc会预分配更多块这样的小块,从而组成一个大块的内存块,这个大块的内存块是page size的整数倍。当然对于大size的class,jemalloc就不会预分配这样的内存块了。而edata_t就是管理这样的整块内存块的数据结构。

下图的场景仍然是main之前的在做初始化时的调用栈,下图是在初始化tsd的tcache data的过程中分配各个size class的tcache_bin用的stack_head指针数组的内存时,需要在具体分配该size的内存对应的内存块的分配前(2.4.1 一节的截图流程),先分配该size class的内存块对应的元数据edata数据结构。

上图里的base_alloc_edata所调用base_alloc_impl分配的大小是很小的,如下图:

就128字节:

和 2.3.2 同样的,由于分配的大小很小,所以直接从base里的当前的block块里剩下的内存里扣出一块出来就可以了,它并不会触发base_map及其他底层分配动作。

在启动过程中,关于edata_t管理结构分配的另外一个场景是在tsd_tcache_data_init时在进行分配各个size class的tcache_bin用的stack_head指针数组的内存对应的内存块时,发现通过ehooks_alloc分配出来的一大块extent内存块后有不用的多出来的部分,这部分剩下的内存也需要创建对应的edata管理结构,在创建该剩下的内存的edata管理结构时走到的edata_cache_get再到base_alloc_edata再到base_alloc_impl里,而这个最后三级函数的调用是和刚才说的场景是一致的,在extent_grow_retained函数内到最后三级函数edata_cache_get->base_alloc_edata->base_alloc_impl中间还有两级调用,为extent_split_interior->extent_split_impl,有关完整调用链可见第三章的调用链图。

2.3.5 base模块会分配用来管理关联每个edata信息所需要的radix tree所用到的叶子结构的内存

jemalloc里有个全局唯一的管理关联每个edata信息的radix tree,即jemalloc里的rtree,这个rtree需要用到rtree_leaf_elm_t结构,所需要的该结构体的数量即radix tree的一层一层的数量,这个数量是按照索引项的bit来决定的,一层是18bit,如下图:

所以一层用到了1<<18=26144个,如下图看到rtree_levels里就两层:

这种情况所触发的函数,如下图,是在rtree_leaf_init函数里:

调用链(也是在初始化时场景):

由于这次分配的大小较大:

所以需要通过pages_map来向os要内存:

上图里的pages_map实际做分配的size是在这次分配的调用链里是由base模块里的pind_last变量来管理维护的,每次分配都要+1,因为+1后,得是HUGEPAGE_CEILING进行2M的对齐,所以就变成了分配4M了,有关pind_last的逻辑具体细节可参考之前的博客 由jemalloc 5.3.0初始化时的内存分配的分析引入jemalloc的三个关键概念及可借鉴的高性能编码技巧-CSDN博客 里 2.2.5 一节。

2.3.6 base模块会分配background线程所用到的background_thread_info_t数据结构

相关的调用截图和调用链截图如下:

2.4 非base分配器分配出内存的调用栈例子

这一节并不是本文的重点,我们并不过多展开,只是把相关调用栈贴出并描述相关调用场景。

这里说的其他分配器,是指离调用os的mmap接口(jemalloc默认只用mmap进行内存分配)进行的内存分配非常近的调用层次的分配函数。jemalloc里pages_map接口封装了这样的os的mmap接口的内存分配,作为一个接口给jemalloc里较底层的分配器所使用。所以,无论是base分配器还是其他的下面会讲到的分配器,最终还是会调用到pages_map接口。

2.4.1 使用extent_alloc_core分配函数

下图调用链是最终用到了extent_alloc_core函数,而extent_alloc_core函数和base_map一样也是调用的extent_alloc_mmap函数,下图调用链所在的场景是在初始化tsd的tcache data的过程中分配各个size class的tcache_bin用的stack_head指针数组的内存对应的内存块,大小在对齐PAGE_SIZE后是32768字节:

上截图也是在初始化流程中,是在 2.3.4 截图流程之后,在 2.3.5 截图的流程之前。

2.4.2 其他调用pages_map的分配器

调用extent_alloc_mmap进行分配的函数就base_map和 2.4.1 说的extent_alloc_core两个。

但是使用pages_map进行分配的函数还有一些,如hpa模块:

hpa_hooks模块:

这里先不展开了。

2.5 base_block_alloc函数有点像分配一个内存池子,每个base都至少有一个内存池子

我们回头来说base_block_alloc函数,上面讲到,base模块里的最终向操作系统去做分配的最终都汇总到了调用这个base_block_alloc函数里。

这个base_block_alloc函数有点像是预分配一个内存池。后面的大大小小的分配都需要在这个池子里进行再分配(只是说这个内存池的管理是按照之前博客说的group delta这样管理的),当然这个池子是可以变大的,但是每一次变大的最小颗粒度很大,之前的博客也说了是至少分配2M,且一次分配比一次分配大,参考之前的 由jemalloc 5.3.0初始化时的内存分配的分析引入jemalloc的三个关键概念及可借鉴的高性能编码技巧-CSDN博客 博客里的 2.2.5 一节。

刚才说的这个内存池子的分配就是下图里的通过base_block_alloc函数得到的base_block_t:

可以从下图中看到,分配出来的block塞到了base_t结构体里的block链表里(下图是因为是第一个block,所以直接赋值就行了):

如果是新增block,则是下图里的红色框逻辑的逻辑(目前新增block只会被base_extent_alloc函数用到):

因为同一个base实例而言,可以新关联base_block_t,所以就有这一节标题里说的每个base都至少有一个内存池子,意思就是可以有大于一个内存池子。

2.6 base_block_alloc函数分配出来的池子是对应base编号的,且base编号是和arena的编号是一一对应的

1)所谓的base_block_alloc函数分配出来的池子是对应base编号的,意思就是base0至少有一个内存池子,如果有base1的话,那么base1也至少有一个内存池子,如果一个base里发现内存不够,可以新申请一个内存池子再与这个base关联。每笔使用base_block_alloc函数进行的内存分配,都需要明确一个所属的base。

如下图,传入给base_block_alloc函数的unsigned ind就是这个base编号:

0是第一个base,第一个base由一个base.c里的static base_t变量来保存是:

2)刚才说的base编号是和arena的编号一一对应的。下图是代码里能体现逻辑上arena的index对应于base的index的代码细节:

3)顺便提及一个arena的细节,对于大size的内存分配,jemalloc把它归到了最后一个index的arena里,即,如果cpu的数量是20,大内存分配用的是最后一个arena即arena80(假设arena0是第一个)。

这块细节我们在以后的博客里会讲到。

2.7 base里挑选可用的空间来进行分配的逻辑

base里的这个挑选的逻辑就一处,在base_alloc_impl里,这个base_alloc_impl就是上面讲过的两个分配元数据的接口base_alloc和base_alloc_edata必经调用到的。

2.7.1 “挑选”逻辑用到了配对堆 Pairing Heap

在base_alloc_impl里,有下面这段逻辑做所谓的“挑选”:

上图里的红色框出的逻辑里,核心函数edata_heap_remove_first是使用了配对堆的数据结构,如下图看到base的avail数组就是配对堆的数据结构:

base_alloc_impl的实现就是从base里的最多SC_NSIZES个配对堆里:

从最小的可能满足size大小的配对堆往size更大的堆去找,直到遇到非空的配对堆就返回堆里最小的元素。至于如何判断堆里元素谁大谁小,见 2.7.3 一节。

我们先看一下配对堆的宏定义和使用。

2.7.2 配对堆的宏定义和使用

jemalloc里除了base模块里的内存管理以外,数据内存块的管理者edata_cache模块和huge page的管理模块hpdata也使用到了配对堆,所以,配对堆的声明总共有3处,两处在edata.h里,另外一处在hpdata.h里:

其中,base模块用的是edata_heap。

配对堆的定义在各自的.c里,如base模块用到的edata_heap是定义在edata.c里,如下图显示了base模块用到的配对堆关联的配对函数edata_snad_comp:

配对堆的一系列函数的函数名是由大量的宏拼接形成的,如下:

2.7.3 base用到的配对堆如何比较谁大谁小?

我们看一下决定base模块用的配对堆里的元素,决定谁大谁小的逻辑,上一节也说到了,该函数是edata_snad_comp:

可以看到它是先比较edata的sn号,关于edata的sn号,我们在上面的 2.2.2 一节里说到了是如何生成的。

比较完sn号,如果是一样的话,再去比较地址大小。

这么比较是为了让早分配的内存更早被释放,可以减少内存碎片。

2.7.4 base分配时选到的配对堆元素可能大于实际需要的大小,相关的塞回配对堆的逻辑介绍

从上面的 2.7.1 一节里就可以看到,base分配时选到的配对堆元素可能大于实际需要的大小。

我们看一下相关的塞回配对堆的逻辑在哪里,base模块用的是base_extent_bump_alloc_post进行的塞回动作。

下图是base_extent_bump_alloc_post的选择塞哪里的逻辑:

可以看到是用的floor,也就是在塞的时候,会塞到它实际所属的index的下一个,这样在分配时拿到的元素肯定都能满足当前这个delta组里的所有size的情况。关于delta以及group等概念见之前的博客 由jemalloc 5.3.0初始化时的内存分配的分析引入jemalloc的三个关键概念及可借鉴的高性能编码技巧-CSDN博客 里的 2.2.4 一节。

三、malloc_init_hard时的所有内存分配动作的调用流程图

细心的同学会发现,上面第二章里的所有截图出来的堆栈调用链都是在malloc_init_hard下的函数调用的。

我们把这个期间的所有的内存分配的逻辑调用都在上面一一列出来了,进行了分析。

下面是一张整图,如果需要再看得清楚一些可以参考我发布的资源里的图,资源链接 https://download.csdn.net/download/weixin_42766184/90385037。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

从零搭建微服务项目(第5章——SpringBoot项目LogBack日志配置+Feign使用)

前言&#xff1a; 本章主要在原有项目上添加了日志配置&#xff0c;对SpringBoot默认的logback的配置进行了自定义修改&#xff0c;并详细阐述了xml文件配置要点&#xff08;只对日志配置感兴趣的小伙伴可选择直接跳到第三节&#xff09;&#xff0c;并使用Feign代替原有RestT…

2024最新版JavaScript逆向爬虫教程-------基础篇之Chrome开发者工具学习

目录 一、打开Chrome DevTools的三种方式二、Elements元素面板三、Console控制台面板四、Sources面板五、Network面板六、Application面板七、逆向调试技巧 7.1 善用搜索7.2 查看请求调用堆栈7.3 XHR 请求断点7.4 Console 插桩7.5 堆内存函数调用7.6 复制Console面板输出 工…

Elasticsearch+Logstash+Kibana可视化集群部署

文章目录 1.组件介绍简述2.集群规划3.Es组件部署4.Logstash组件部署5.Kibana组件部署6.Kibana的基础使用 1.组件介绍简述 Elasticsearch&#xff1a;开源实时分布式搜索和分析引擎&#xff0c;支持大规模数据存储和高吞吐量&#xff0c;提供丰富的搜索功能和可扩展性。 Logsta…

08模拟法 + 技巧 + 数学 + 缓存(D3_数学)

目录 1. 多数元素 1.1. 题目描述 1.2. 解题思路 方法一&#xff1a;哈希表 方法二&#xff1a;排序 方法三&#xff1a;随机化 方法四&#xff1a;分治 方法五&#xff1a;Boyer-Moore 投票算法 2. 按规则计算统计结果 2.1. 题目描述 2.2. 解题思路 3. 整数拆分 3.…

基于IOS实现各种倒计时功能

ZJJTimeCountDown 效果图 特点&#xff1a; 1、已封装&#xff0c;支持自定义 2、支持文本各种对齐模式 3、各种效果都可以通过设置 ZJJTimeCountDownLabel 类属性来实现 4、支持背景图片设置 5、分文本显示时间时&#xff0c;支持设置文字大小&#xff0c;来动态设置每个文本…

【TS合成MP4】你怎么专打裂开的切片呀

写在前面&#xff1a;本博客仅作记录学习之用&#xff0c;部分图片来自网络&#xff0c;如需引用请注明出处&#xff0c;同时如有侵犯您的权益&#xff0c;请联系删除&#xff01; 文章目录 前言TS与MP4格式概述TS与MP4格式概述TS合成MP4的需求背景TS合成MP4的方法概述 合并方法…

【动手学强化学习】01初探强化学习

文章目录 什么是强化学习强化学习解决的问题强化学习的独特性 什么是强化学习 强化学习是机器通过与环境交互来实现目标的计算方法。智能体与环境的交互方式如图所示&#xff0c;在每一轮交互中&#xff0c;智能体根据感知状态经过自身计算给出本轮动作&#xff0c;将其作用于…

C++,STL容器适配器,priority_queue:优先队列深入解析

文章目录 一、容器概览与核心特性核心特性速览二、底层实现原理1. 二叉堆结构2. 容器适配器架构三、核心操作详解1. 容器初始化2. 元素操作接口3. 自定义优先队列四、实战应用场景1. 任务调度系统2. 合并K个有序链表五、性能优化策略1. 底层容器选择2. 批量建堆优化六、注意事项…

duckdb导出Excel和导出CSV速度测试

运行duckdb数据库 D:>duckdb v1.2.0 5f5512b827 Enter “.help” for usage hints. Connected to a transient in-memory database. Use “.open FILENAME” to reopen on a persistent database. 生成模拟数据&#xff0c;10个列&#xff0c;100万行数据&#xff1b; --…

k8s集群离线安装kuberay operator

1,安装方式 采用helm安装方式&#xff0c;首先下载对应的helm chart&#xff0c;这里采用v1.2.2版本&#xff0c;下载地址&#xff1a; https://github.com/ray-project/kuberay-helm/releases/tag/kuberay-operator-1.2.2 2,解压并修改镜像源 由于是在内网环境下搭建&#…

结构形模式---适配器模式

适配器模式是一种结构形模式&#xff0c;主要用于不同在两个互不兼容的类或者库之间增加一个转换。 适配器模式的实现由两种方式&#xff0c;一种是适配器对象&#xff0c;一种是适配器类。 适配器是对象是将第三方接口通过对象调用引入到适配器中。 适配器类是通过多继承将…

面向SDV的在环测试深度解析——概述篇

1.引言 在汽车行业迈向软件定义汽车&#xff08;SDV&#xff09;的进程中&#xff0c;传统的硬件在环&#xff08;HIL&#xff09;测试方案在面对新的技术架构和需求时逐渐显露出局限性。一方面&#xff0c;现代汽车的电子电气架构日益复杂&#xff0c;高性能计算&#xff08;…

2025年智慧城市解决方案下载:AI-超脑中台,体系架构整体设计

2025年&#xff0c;随着人工智能、物联网、大数据等新兴技术的深度融合&#xff0c;智慧城市解决方案正迈向更高层次的智能化和协同化阶段。其中&#xff0c;AI-超脑中台作为核心架构的一部分&#xff0c;为城市智能化运行提供了强大支撑。 智慧城市最新解决方案&#xff0c;标…

LINUX常用命令学习

查看系统版本 使用hostnamectl命令检查。hostnamectl显示了CentOS的版本以及操作系统的相关信息&#xff0c;非常方便 设置linux机器别名称 hostnamectl set-hostname 机器别名 --static 华为云 centos 命令&#xff1a;lsb_release -a linux:cat /proc/version 查看进程路…

RK3588 Linux平台部署DeepSeek模型教程

更多内容可以加入Linux系统知识库套餐&#xff08;教程&#xff0b;视频&#xff0b;答疑&#xff09; 文章目录 一、下载rknn-llm 和 deepseek模型二、RKLLM-Toolkit 安装2.1 安装 miniforge3 工具2.2 下载 miniforge3 安装包2.3 安装 miniforge3 三、创建 RKLLM-Toolkit Cond…

Azure从0到1

我能用Azure做什么? Azure提供100多种服务,能够从在虚拟机上运行现有应用程序到探索新的软件范式,如智能机器人和混合现实。许多团队开始通过将现有应用程序移动到在Azure中运行的虚拟机(VM)来探索云。将现有应用程序迁移到虚拟机是一个良好的开端,但云不仅仅是运行虚拟…

智慧城市V4系统小程序源码独立版全插件全开源

智慧城市V4系统小程序源码&#xff1a;多城市代理同城信息服务的全域解决方案 在数字化浪潮的推动下&#xff0c;智慧城市已成为全球发展的核心战略。作为这一领域的革新者&#xff0c;智慧城市V4系统小程序源码凭借其多城市代理同城信息服务能力与多商家营销功能&#xff0c;…

JAVA-Lambda表达式(高质量)

要了解Lambda表达式,首先需要了解什么是函数式接口&#xff0c;函数式接口定义&#xff1a;一个接口有且只有一个抽象方法 。 一、函数式接口 1.FunctionalInterger 注意&#xff1a; 1. 如果一个接口只有一个抽象方法&#xff0c;那么该接口就是一个函数式接口 2. 如果我们…

机器视觉--Halcon变量的创建与赋值

一、引言 在机器视觉领域&#xff0c;Halcon 作为一款强大且功能丰富的软件库&#xff0c;为开发者提供了广泛的工具和算子来处理各种复杂的视觉任务。而变量作为程序中存储和操作数据的基本单元&#xff0c;在 Halcon 编程中起着至关重要的作用。正确地创建和赋值变量是编写高…

优选驾考小程序

第2章 系统分析 2.1系统使用相关技术分析 2.1.1Java语言介绍 Java语言是一种分布式的简单的 开发语言&#xff0c;有很好的特征&#xff0c;在安全方面、性能方面等。非常适合在Internet环境中使用&#xff0c;也是目前企业级运用中最常用的一个编程语言&#xff0c;具有很大…