【AI系统】Ascend C 语法扩展

Ascend C 语法扩展

Ascend C 的本质构成其实是标准 C++加上一组扩展的语法和 API。本文首先对 Ascend C 的基础语法扩展进行简要介绍,随后讨论 Ascend C 的两种 API——基础 API 和高阶 API。

接下来针对 Ascend C 的几种关键编程对象——数据存储、任务间通信与同步,资源管理以及临时变量进行详细解读,为后续讲解 Ascend C 的编程范式打下理论基础。

语法扩展概述

Ascend C 采用华为自研的毕昇编译器,设备侧编程采用 C/C++语法扩展允许函数执行空间和地址空间作为合法的类型限定符,提供在主机(Host)侧和设备(Device)侧独立执行的能力,同时提供针对不同地址空间的访问能力。

函数执行限定符

函数执行空间限定符指示函数是在主机上执行还是在设备上执行,以及它是否可从主机或设备调用。主要有以下三种声明方式:

  • 首先是__global__执行空间限定符。它声明一个 kernel 函数(Ascend C 算子的入口函数,后文会详细介绍),具有如下性质:

    1. 在设备上执行;
    2. 只能被主机侧函数调用;
    3. __global__只是表示这是设备侧函数的入口,并不表示具体的设备类型;
    4. 一个__global__函数必须返回 void 类型,并且不能是 class 的成员函数;
    5. 主机侧调用__global__函数必须使用<<<>>>(内核调用符)异构调用语法;
    6. __global__的调用是异步的,意味着函数返回,并不表示 kernel 函数在设备侧已经执行完成,如果需要同步,须使用运行时提供的同步接口显式同步,如aclrtSynchronizeStream(…)
  • 第二类是__aicore__执行空间限定符。它声明的函数,具有如下属性。

    1. 在 AI Core 设备上执行;
    2. 只能被__global__函数,或者其他 AI Core 函数调用。
  • 第三类是__host__执行空间限定符。它声明的函数(通常不显示声明)具有如下属性。

    1. 只能在主机侧执行;
    2. 只能被主机侧函数调用;
    3. __global____host__不能一起使用。

典型使用函数执行空间限定符的示例:

//定义 aicore 函数
__aicore__ void bar() {}

// 定义核函数
__global__ __aicore__ void foo() { bar();}

//定义 Host 函数
int () {}

地址空间限定符

地址空间限定符可以在变量声明中使用,用于指定对象分配的区域。AI Core 具备多级独立片上存储,各个地址空间独立编址,具备各自的访存指令。如果对象的类型被地址空间名称限定,那么该对象将被分配在指定的地址空间中。同样地,对于指针,指向的类型可以通过地址空间进行限定,以指示所指向的对象所在的地址空间。

private 地址空间:private 地址空间是大多数变量的默认地址空间,特别是局部变量,代码中不显示标识所在局部地址空间类型。

__g'm__地址空间__gm__地址空间限定符用来表示分配于设备侧全局内存的对象,全局内存对象可以声明为标量、用户自定义结构体的指针。

Ascend C API 概述

Ascend C 算子采用标准 C++ 语法和一组编程类库 API 进行编程,可以根据自己的需求选择合适的 API。Ascend C 编程类库 API 示意图如下图所示,Ascend C API 的操作数都是 Tensor 类型:GlobalTensor(外部数据存储空间)和 LocalTensor(核上内存空间);类库 API 分为高阶 API 和基础 API。

在这里插入图片描述

基础 API

实现基础功能的 API,包括计算类、数据搬运、内存管理和任务同步等。使用基础 API 自由度更高,可以通过 API 组合实现自己的算子逻辑。基础 API 是对计算能力的表达。

  1. 基础 API 分类
  • 计算类 API:包括标量计算 API、向量计算 API、矩阵计算 API,分别实现调用标量计算单元、向量计算单元、矩阵计算单元执行计算的功能。

  • 数据搬运 API:计算 API 基于本地内存(Local Memory)数据进行计算,所以数据需要先从全局内存(Global Memory)搬运至本地内存,再使用计算接口完成计算,最后从本地内存搬出至全局内存。执行搬运过程的接口称之为数据搬运接口,比如 DataCopy 接口。

  • 内存管理 API:用于分配板上管理内存,比如 AllocTensor、FreeTensor 接口。这个 API 的出现是由于板上内存较小,通常无法存储完整数据,因此采用动态内存的方式进行内存管理,实现板上内存的复用。

  • 任务同步 API:完成任务间的通信和同步,比如 EnQue、DeQue 接口。不同的 API 指令间有可能存在依赖关系,不同的指令异步并行执行,为了保证不同指令队列间的指令按照正确的逻辑关系执行,需要向不同的组件发送同步指令。任务同步类 API 内部即完成这个发送同步指令的过程,开发者无需关注内部实现逻辑,使用简单的 API 接口即可完成。

  1. 基础 API 计算方式

对于基础 API 中的计算 API,根据对数据操作方法的不同,分为下表所示的几种计算方式:

命名说明
整个 Tensor 参与计算整个 Tensor 参与计算:通过运算符重载的方式实现,支持+, -, *, /, |, &, <, >, <=, >=, ==, !=,实现计算的简化表达。例如:dst=src1+src2
Tensor 前 n 个数据计算Tensor 前 n 个数据计算:针对源操作数的连续 n 个数据进行计算并连续写入目的操作数,解决一维 Tensor 的连续计算问题。例如:Add(dst, src1, src2, n);
Tensor 高维切分计算功能灵活的计算 API,充分发挥硬件优势,支持对每个操作数的 Repeat times(迭代的次数)、 Block stride(单次迭代内不同 block 间地址步长)、Repeat stride(相邻迭代间相同 block 的地址步长)、Mask(用于控制参与运算的计算单元)的操作。

下图以向量加法计算为例,展示了不同级别向量计算类 API 的特点。从图中我们可以初步看出,Tensor 高维切分计算操作单元最小,可以针对不同步长实现最为细致的操作,针对 Tensor 高维切分计算更加细致的介绍会在附录 Ascend C API 中呈现。

Tensor 前 n 个数据计算可以实现一维的连续计算,可以指定 Tensor 的特定长度参与计算,Tensor 前 n 个数据计算也是一般开发过程中使用最为频繁的接口,兼具较强的功能性和易用性;整个 Tensor 参与计算是易用性最强的,使用难度最低的,针对整个 Tensor 进行计算,但是功能性较低。开发者可以根据自身水平和不同的需要去灵活地选择各种层级的接口。

在这里插入图片描述

高阶 API

封装常用算法逻辑的 API,比如 Matmul、Softmax 等,可减少重复开发,提高开发者开发效率。使用高阶 API 可以快速的实现相对复杂的算法逻辑,高阶 API 是对于某种特定算法的表达。

例如使用高阶 API 完成 Matmul 算子时,需要创建一个矩阵乘法类进行运算,其中入参包含两个相乘的矩阵(一般称为 A 矩阵与 B 矩阵)信息、输出结果矩阵(一般称为 C 矩阵)信息、矩阵乘偏置(一般称为 Bias)信息,上述信息中包括了对应矩阵数据的内存逻辑位置、数据存储格式、数据类型、转置使能参数。

创建完这样的一个矩阵乘法类后,使用 Ascend C 高阶 API 可以直接完成对左右矩阵 A、B 和 Bias 的设置和矩阵乘法操作以及结果的输出,开发者不用再自主实现复杂的数据通路和运算操作。

数据存储

根据 Ascend C 对于 AI Core 的硬件抽象设计,AI Core 内部的存储统一用 Local Memory 来表示,AI Core 外部的存储统一用 Global Memory 来表示。

Ascend C 使用 GlobalTensor 作为 Global Memory 的数据基本操作单元,与之对应的,用 LocalTensor 作为 Local Lemory 的数据基本操作单元。数据的基本操作单元(Tensor,张量)是各种指令 API 直接处理的对象,也是数据的载体。本节具体介绍这两个关键的数据结构的原型定义和用法。

外部存储数据空间

外部存储数据空间(GlobalTensor)用来存放 AI Core 外部存储(Global Memory)的全局数据。

其原型定义如下:

template <typename T> class GlobalTensor {
	void SetGlobalBuffer(__gm__ T* buffer, uint32_t bufferSize); 
	const __gm__ T* GetPhyAddr();                                
	uint64_t GetSize();                                          
	GlobalTensor operator[](const uint64_t offset);              
}

在上边的程序中,第 2 行代码的作用是传入全局数据的指针,并手动设置一个 buffer size,初始化 GlobalTensor;第 3 行代码的作用是返回全局数据的地址类型 T 支持所有数据类型,但需要遵循使用此 GlobalTensor 的指令的数据类型支持情况;第 4 行代码的作用是返回 Tensor 中的 element 个数;第 5 行代码的作用是指定偏移返回一个 GlobalTensor,offset 单位为 element。

核内数据存储空间

核内数据存储空间(LocalTensor)用于存放 AI Core 中内部存储(Local Memory)的数据。其原型定义如下:

template <typename T> class LocalTensor {
	T GetValue(const uint32_t offset) const;
	template <typename T1> void SetValue(const uint32_t offset, const T1 value) const;
	LocalTensor operator[](const uint32_t offset) const;
	uint32_t GetSize() const;
	void SetUserTag(const TTagType tag);
	TTagType GetUserTag() const;
}

在上边的程序片段中,第 2 行代码的作用是获取 LocalTensor 中的某个值,返回 T 类型的立即数;第 3 行代码的作用是设置LocalTensor中的某个值,offset 单位为 element;第 4 行代码的作用是获取距原LocalTensor起始地址偏移量为 offset 的新LocalTensor,注意 offset 不能超过原有LocalTensor的 size 大小,offset 单位为 element;第 5 行代码的作用是获取当前LocalTensor尺寸大小。第 6 行代码的作用是让开发者自定义设置 Tag 信息;第 7 行代码的作用是获取 Tag 信息。

任务通信与同步

Ascend C 使用 TQue 队列完成任务之间的数据通信和同步,在 TQue 队列管理不同层级的物理内存时,用一种抽象的逻辑位置(TPosition)来表达各级别的存储,代替了片上物理存储的概念,开发者无需感知硬件架构。

TPosition 类型包括:VECIN、VECCALC、VECOUT、A1、A2、B1、B2、CO1、CO2。其中 VECIN、VECCALC、VECOUT 主要用于向量编程;A1、A2、B1、B2、CO1、CO2 用于矩阵编程。TPosition 的枚举值定义见下表。

TPosition具体含义
GM全局内存(Global Memory),对应 AI Core 的外部存储。
VECIN用于向量计算,搬入数据的存放位置,在数据搬入矩阵计算单元时使用此位置
VECOUT用于向量计算,搬出数据的存放位置,在将矩阵计算单元结果搬出时使用此位置
VECCALC用于向量计算/矩阵计算,在计算需要临时变量时使用此位置
A1用于矩阵计算,存放整块 A 矩阵,可类比 CPU 多级缓存中的二级缓存
B1用于矩阵计算,存放整块 B 矩阵,可类比 CPU 多级缓存中的二级缓存
A2用于矩阵计算,存放切分后的小块 A 矩阵,可类比 CPU 多级缓存中的一级缓存
B2用于矩阵计算,存放切分后的小块 B 矩阵,可类比 CPU 多级缓存中的一级缓存
CO1用于矩阵计算,存放小块结果 C 矩阵,可理解为 Cube Out
CO2用于矩阵计算,存放整块结果 C 矩阵,可理解为 Cube Out

一个使用 TQue 数据结构的样例程序如下所示:

TQue<TPosition::VECIN, BUFFER_NUM> que;
LocalTensor<half> tensor1 = que.AllocTensor();
que.FreeTensor<half>(tensor1);
que.EnQue(tensor1);
LocalTensor<half> tensor1 = que.DeQue<half>();

在上边程序片段中呈现的内容只是 TQue 数据结构使用的一个样例,读者需要根据自己的需求在程序中合理的位置进行使用。其中第 1 行代码的作用是创建队列,VECIN 是 TPosition 枚举中的一员;BUFFER_NUM 参数是队列的深度,que 参数是自定义队列名称;第 2 行代码的作用是使用 TQue 的一个功能:

AllocTensor(),在片上分配空间给一个 LocalTensor,分配的默认大小为 que 在初始化时设置的一块的大小,也可以手动设置大小的值但是不能超过分配的大小;第 3 行代码的作用是释放 Tensor,使用 FreeTensor() 方法,需要与 AllocTensor() 成对使用;第 4 行代码的作用是将 LocalTensor 加入到指定队列中,从而利用队列来进行不同任务之间的数据同步与通信;第 5 行代码的作用是将 LocalTensor 从队列中取出,以能够进行后续的计算等操作。

资源管理

在 Ascend C 中,流水任务间数据传递使用到的内存统一由资源管理模块 TPipe 进行管理。TPipe 作为片上内存管理者,通过 InitBuffer 接口对外提供 TQue 内存初始化功能,开发者可以通过该接口为指定的 TQue 分配内存。

TQue 队列内存初始化完成后,需要使用内存时,通过调用 AllocTensor 来为 LocalTensor 分配内存,当创建的 LocalTensor 完成相关计算无需再使用时,再调用 FreeTensor 来回收 LocalTensor 的内存。内存管理示意图如下图所示:

在这里插入图片描述

一个使用 TPipe 数据结构的样例展示如下所示:

TPipe pipe;
pipe.InitBuffer(que, num, len);

在上述程序中呈现的内容只是 Tbuf 数据结构使用的一个样例,读者需要根据自己的需求在程序中合理的位置进行使用。其中第 1 行代码的作用是实例化 TPipe 数据结构,名称为 pipe;第 2 行代码的作用是使用 TPipe 的一个功能:初始化片上内存(队列),其中参数 que 为指定的已经创建的队列名称;参数 num 为分配的内存块数,num=2 时开启 double buffer 优化;参数 len 为分配的一块内存大小(单位 Bytes)。

在这里简单介绍一下 double buffer 优化机制。执行于 AI Core 上的指令队列主要包括如下几类,即 Vector 指令队列(V)、Matrix 指令队列(M)和存储移动指令队列(MTE2、MTE3)。不同指令队列间的相互独立性和可并行执行特性,是 double buffer 优化机制的基石。double buffer 基于 MTE 指令队列与 Vector 指令队列的独立性和可并行性,通过将数据搬运与 Vector 计算并行执行以隐藏数据搬运时间并降低 Vector 指令的等待时间,最终提高 Vector 单元的利用效率。

临时变量

使用 Ascend C 编程的过程中,可能会用到一些临时变量,例如在 Compute 阶段开发者会使用到一些复杂的数据结构。这些临时变量占用的内存可以使用 TBuf 来管理,存储位置通过模板参数来设置,可以设置为不同的 TPosition 逻辑位置。

在使用 TBuf 时,建议将临时变量初始化成为算子类成员中的一个,不需要重复地申请与释放,能达到提升算子性能的效果。

TBuf 占用的存储空间通过 TPipe 进行管理,您可以通过 InitBuffer 接口为 TBuf 进行内存初始化操作,之后即可通过 Get 获取指定长度的 Tensor 参与计算。

使用 InitBuffer 为 TBuf 分配内存和为 TQue 分配内存有以下差异:

  • TBuf 分配的内存空间只能参与计算,无法执行 TQue 队列的入队出队操作。

  • 调用一次内存初始化接口,TPipe 只会为 TBuf 分配一块内存,TQue 队列可以通过参数设置申请多块内存。如果要使用多个临时变量,需要定义多个 TBuf 数据结构,对每个 Tbuf 数据结构分别调用 InitBuffer 接口进行内存初始化。

  • 使用 TBuf 时可以不需要重复进行申请释放内存操作。

一个使用 TBuf 数据结构的样例展示如下所示:

TBuf<TPosition::pos> calcBuf;
pipe.InitBuffer(calcBuf, len);
LocalTensor<half> temtensor1 = calcBuf.Get<half>();
LocalTensor<half> temtensor1 = calcBuf.Get<half>(128);

在上一段程序中呈现的内容只是 TBuf 数据结构使用的一个样例,读者需要根据自己的需求在程序中合理的位置进行使用。其中第 1 行代码的作用是进行临时变量声明,其中 pos 参数为队列逻辑位置 TPosition,可以为 VECIN、VECCALC、VECOUT、A1、A2、B1、B2、CO1、CO2。第 2 行代码的作用是进行临时变量初始化,pipe 为实例化的一个 TPipe 数据结构,同样使用 TPipe 数据结构中的 InitBuffer 操作对临时变量进行初始化,但只需要声明名称和长度 len 即可,长度的单位仍然是 Bytes。第 3、4 行代码的作用是分配临时的 LocalTensor,使用 TBuf 数据结构中的 Get() 方法进行临时 Tensor 的分配,若不引入入参则分配的空间大小为初始化时 len 的大小,单位为 Bytes,若引入入参,可以指定长度地分配临时 Tensor 的大小,但是长度不能超过 len。

如果您想了解更多AI知识,与AI专业人士交流,请立即访问昇腾社区官方网站https://www.hiascend.com/或者深入研读《AI系统:原理与架构》一书,这里汇聚了海量的AI学习资源和实践课程,为您的AI技术成长提供强劲动力。不仅如此,您还有机会投身于全国昇腾AI创新大赛和昇腾AI开发者创享日等盛事,发现AI世界的无限奥秘~

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

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

相关文章

【合作原创】使用Termux搭建可以使用的生产力环境(二)

前言 上期文章没看的可以先从上期文章开始看起 【合作原创】使用Termux搭建可以使用的生产力环境&#xff08;一&#xff09;-CSDN博客 目前我们已经完成了FinalShell ssh连接手机Termux的功能了&#xff0c;这期我们继续朝我们的目标前进。今天早上有读者进群以为生成环境指…

UG NX二次开发(C#)-选择对象居中(不是全部居中)

文章目录 1、前言2、什么是对象居中3、功能实现代码3.1 对象居中3.1 恢复原视图1、前言 在UG NX二次开发过程中,我们经常会用到居中以查看完整的模型,但是对于如果想展示某些对象,而不是全部模型时,那么我们就想将选择的对象(如体对象)居中查看,当查看结束后还能恢复到…

群控系统服务端开发模式-应用开发-短信工厂七牛云短信开发

一、七牛云短信工厂开发 1、添加框架对应的SDK composer require qiniu/php-sdk 2、添加七牛云工厂 在根目录下extend文件夹下Sms文件夹下channel文件夹下&#xff0c;创建七牛云短信发送工厂并命名为QiniuyunSmsSender。记住&#xff0c;一定要在七牛云短信发送工厂类名后面去…

机器学习概述,特征工程简述2.1——2.3

机器学习概述&#xff1a; 1.1人工智能概述 达特茅斯会议—人工智能的起点 机器学习是人工智能的一个实现途径 深度学习是机器学习的一个方法发展而来 1.1.2 机器学习和深度学习能做什么 传统预测 图像识别 自然语言处理 1.2什么是机器学习 数据 模型 预测 从历史数…

基于vite6+ vue3 + electron@33 实现的 局域网内互传文件的桌面软件

目录 项目介绍项目部分截图介绍下基础项目搭建先搭建一个vite 前端项目 再安装 electron 相关依赖依赖安装失败解决方案修改 vite配置文件和 ts 配置文件修改packjsonts相关配置项目结构介绍 项目介绍 前端 基于 vue3 ts windicss 后端 就是node 层 项目地址&#xff1a; h…

Linux 内核系统架构

Linux 内核是一个复杂且高度模块化的系统&#xff0c;负责操作硬件资源、管理进程和内存、提供网络服务、执行文件系统操作、进行设备驱动程序的管理等。它为用户空间提供了一个抽象层&#xff0c;并为应用程序提供了底层服务。本文将深入探讨 Linux 内核的系统架构&#xff0c…

TYUT设计模式精华版

七大原则 单一职责原则 职责要单一不能将太多的职责放在一个类中 开闭原则 软件实体对扩展是开放的&#xff0c;但对修改是关闭的 里氏代换原则 一个可以接受基类对象的地方必然可以接受子类 依赖倒转原则 要针对抽象层编程&#xff0c;而不要针对具体类编程 接口隔离原则 …

计算机网络——不同版本的 HTTP 协议

介绍 HTTP&#xff0c;即超文本传输协议&#xff08;HyperText Transfer Protocol&#xff09;&#xff0c;是应用层的一个简单的请求-响应协议&#xff0c;它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。本文将介绍 HTTP 协议各个版本。 HTTP/1.0 HTTP/1…

Fastapi + vue3 自动化测试平台---移动端App自动化篇

概述 好久写文章了&#xff0c;专注于新框架&#xff0c;新UI界面的实践&#xff0c;废话不多说&#xff0c;开搞 技术架构 后端&#xff1a; Fastapi Airtest multiprocessing 前端&#xff1a; 基于 Vue3、Vite、TypeScript、Pinia、Pinia持久化插件、Unocss 和 Elemen…

FreeRTOS之ARM CR5栈结构操作示意图

FreeRTOS之ARM CR5栈结构操作示意图 1 FreeRTOS源码下载地址2 ARM CR5栈结构操作宏和接口2.1 portSAVE_CONTEXT宏2.1.1 portSAVE_CONTEXT源码2.1.2 portSAVE_CONTEXT宏操作栈结构变化示意图 2.2 portRESTORE_CONTEXT宏2.2.1 portRESTORE_CONTEXT源码2.2.2 portRESTORE_CONTEXT宏…

警惕开源信息成为泄密源头

文章目录 前言一、信息公开需谨慎1、警惕采购招标泄密。2、警惕信息公开泄密。3、警惕社交媒体泄密。 二、泄密风险需严防1、健全制度&#xff0c;明确责任。2、加强管控&#xff0c;严格审查。3、提高意识&#xff0c;谨言慎行。 前言 大数据时代&#xff0c;信息在网络空间发…

指针(上)

目录 内存和地址 指针变量和地址 取地址&#xff08;&&#xff09; 解引用&#xff08;*&#xff09; 大小 类型 意义 const修饰 修饰变量 修饰指针 指针运算 指针- 整数 指针-指针 指针的关系运算 野指针 概念 成因 避免 assert断言 指针的使用 strl…

常见的数据结构---队列、树与堆的深入剖析

目录 一、队列 二、树 三、堆 在现代计算机科学与工程领域&#xff0c;队列、树和堆是三种极其重要的基础数据结构&#xff0c;它们各自具有独特的特点和应用。在日常开发中&#xff0c;合理选择和使用这些数据结构可以显著提高程序的效率和可维护性。它们不仅奠定了算法设计…

Java 整合图片处理相关一套:传输、保存、重命名、删除、AI图片文字识别、图片映射、vue3裁剪、设置黑白色、设置负片、提高照片品质

目录 一、vue3 axios spring boot文件传输 二、Java图片保存到本地 三、Java 本地图片重命名 四、Java 删除本地图片 五、 JavaAI图片文字识别 六、Java映射图片地址&#xff0c;前端直接访问图片 七、vue3 图片裁剪 八、Java 设置图片黑白色 九、Java 设置图片负片 …

web三、 window对象,延时器,定时器,时间戳,location对象(地址),本地存储-localStorage,数组去重new Set

一、 window对象 window对象 是一个全局对象&#xff0c;也可以说是JavaScript中的 顶级对象 像document、alert()、console.log()这些都是window的属性&#xff0c;基本BOM的属性和方法都是window的 所有通过 var定义 在全局作用域中的 变量 、 函数 都会变成window对象的属…

【AI系统】昇腾异构计算架构 CANN

昇腾异构计算架构 CANN 本文将介绍昇腾 AI 异构计算架构 CANN&#xff08;Compute Architecture for Neural Networks&#xff09;&#xff0c;这是一套为高性能神经网络计算需求专门设计和优化的架构。CANN 包括硬件层面的达芬奇架构和软件层面的全栈支持&#xff0c;旨在提供…

Spark和MapReduce场景应用和区别

文章目录 Spark和MapReduce场景应用和区别一、引言二、MapReduce和Spark的应用场景1. MapReduce的应用场景2. Spark的应用场景 三、MapReduce和Spark的区别1. 内存使用和性能2. 编程模型和易用性3. 实时计算支持 四、使用示例1. MapReduce代码示例2. Spark代码示例 五、总结 Sp…

CSS函数

目录 一、背景 二、函数的概念 1. var()函数 2、calc()函数 三、总结 一、背景 今天我们就来说一说&#xff0c;常用的两个css自定义属性&#xff0c;也称为css函数。本文中就成为css函数。先来看一下官方对其的定义。 自定义属性&#xff08;有时候也被称作CSS 变量或者级…

【C语言】递归的内存占用过程

递归 递归是函数调用自身的一种编程技术。在C语言中&#xff0c;递归的实现会占用内存栈&#xff08;Call Stack&#xff09;&#xff0c;每次递归调用都会在栈上分配一个新的 “栈帧&#xff08;Stack Frame&#xff09;”&#xff0c;用于存储本次调用的函数局部变量、返回地…

大数据新视界 -- 大数据大厂之 Hive 数据压缩算法对比与选择(下)(20 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…