2
DCU技术达人
海光DCU进阶:存储结构与内存管理深度解析——从Bank冲突到页锁定内存
2026-09-20 20:08:40
50
摘要:本文深入解析海光DCU的存储结构与内存管理机制,涵盖可编程存储金字塔、四种存储器的特性对比、共享内存Bank冲突的原理与规避策略、存储修饰符与函数修饰符的使用规范、全局内存的动态管理与Symbol变量、页锁定内存的性能优势,以及同步与异步数据传输的最优实践。旨在帮助开发者理解DCU的存储层次,掌握高效的内存管理技巧。 **关键词:**海光DCU;HIP编程;共享内存;Bank冲突;页锁定内存;存储结构;数据传输 --- ## **引言** 在上一篇《第一个程序与执行模型》中,我们解决了“线程如何组织”的问题。但一个高性能的DCU程序,光有正确的线程组织远远不够——**性能的瓶颈往往出现在“数据放在哪儿、怎么访问”上**。第一篇《架构详解》中反复强调过一个判断:**DCU程序的瓶颈,往往不在计算,而在数据读取。**本文正是把这句话落实到代码层面:深入剖析DCU提供了哪几种存储、它们各自的特性是什么、以及如何高效地利用它们——尤其是**共享内存的bank冲突**和**全局内存与主机的数据传输**这两个核心重点。 ## **一、可编程存储与不可编程存储** 对开发人员来说,DCU上的存储器可以清晰地分为两类: * **可编程的:**需要显式地控制哪些数据存放在里面,例如全局内存和共享内存。 * **不可编程的:**程序自动决定数据放在哪里,无法直接干预,例如L1和L2缓存。 这是一个与CPU截然不同的设计理念。在CPU的内存层次中,各级缓存都是不可编程的——编写代码时感觉不到它们的存在,一切由硬件自动完成。而在DCU上,**共享内存是“可编程的缓存”**:它和缓存一样快,但要不要用它、怎么用它,完全由开发者决定。这份额外的控制权,既是DCU能榨出高性能的原因,也是它编程门槛更高的原因。 DCU的可编程存储结构可以看作一个金字塔,如图3-7所示: ![attachmentId-Redirect.png](/api-forum/file/RedirectImage?path=20260920/1789907464476_Redirect.png) *图3-7 可编程存储结构* 金字塔的三条边清晰地揭示了一个规律:**越往上,访问速度越快、延迟越低,但容量越小;越往下,容量越大,但延迟越高。** | **层级** | **速度** | **容量** | **归属** | **生命周期** | | ------------------- | -------------- | -------------- | --------------- | ------------------ | | 寄存器 | 最快 | 最小 | 线程私有 | 随线程 | | 共享内存 | 很快 | 64 KB / CU | 线程块内共享 | 随线程块 | | 全局内存 / 本地内存 | 慢 | 最大 | 全局 / 线程私有 | 随程序 / 随线程 | ## **二、四种存储的脾气** ### 2.1 寄存器:最快,但会“溢出” 寄存器是DCU上访问速度最快的内存空间。**什么变量会进寄存器?** * 如果核函数中声明的自变量没有其他修饰符,那么它通常存储在寄存器中。 * 在核函数声明的数组中,如果用于引用该数组的索引是**常量且能在编译时确定**,那么该数组也存储在寄存器中。 寄存器变量对于每个线程来说都是**私有**的,通常用来保存需要频繁访问的线程私有变量。它的生命周期与线程相同——**一旦线程执行完毕,就不能再访问它的寄存器变量了。** **寄存器溢出(register spilling):**寄存器数量是有限的(回忆第一篇:每线程最多256个矢量寄存器)。**如果使用了超过硬件限制数量的寄存器,则会用本地内存替代寄存器**——这种寄存器溢出会给性能带来不利影响。为什么不利?下一节讲本地内存时就会明白。 ### 2.2 全局内存:最大、最常用、最慢 全局内存是DCU中容量最大、延迟最高、并且最常使用的内存。它常驻于DCU显存中。“全局”这个词有两层含义:**它贯穿于程序的整个生命周期,并且可以被任何设备(线程)访问**。前面写的核函数,就是在全局内存空间中运行的。全局内存可通过内存事务进行访问,这些内存事务**最好是4字节对齐**——这样可以利用全局内存**合并访存**的特性来减少访问次数。合并访存是全局内存优化的核心,我们放到第三篇专门讲。这里只需记住一句话: > **优化全局内存事务,对于获得最优性能来说是至关重要的。** ### 2.3 共享内存:可编程的缓存 共享内存是HIP编程模型中提供的**可编程缓存**。在DCU上,每个CU都有一块共享内存,大小为**64 KB**,**其带宽和延迟远远优于全局内存**。它在核函数的范围内声明,是**线程之间相互通信的基本方式**。其生命周期伴随着整个线程块——一个块内的线程,通过使用共享内存中的数据可以相互合作。共享内存是本文的重点,第三、四节会专门展开。 ### 2.4 本地内存:名字骗人,其实很慢 “本地内存”这个名字具有很强的误导性。听起来它应该离线程很近、很快,但事实恰恰相反: > **本地内存,位于全局内存上的一个空间。在逻辑上,它属于线程私有,但物理上它和全局内存在同一块存储区域,因此高延迟、低带宽。** 它是怎么产生的?这就接上了2.1节的寄存器溢出: * 当一个线程执行核函数时,核函数的变量、数组、结构体**首先会考虑放在寄存器中**。 * 但当定义的变量、数组、结构体**过大**,编译器判断这部分数据过度占用寄存器资源时,就会把它们**直接存放在本地内存中**。 换句话说: ```text 变量少,寄存器放得下 → 放寄存器(飞快) 变量多/数组大,寄存器放不下 → 溢出到本地内存(= 全局内存,很慢) ``` 这就是为什么“寄存器溢出会损害性能”——**溢出的本质,是把本该在最快存储里的数据,踢到了最慢的存储里。**所以在核函数里定义大数组、大结构体时要格外小心。 ## **三、共享内存与bank:一个“多体交叉存储器”** 现在进入本文的核心。共享内存之所以快,是因为它的物理结构是**并行的**;而它的坑,也正来自这个并行结构。学过计算机组成原理的读者会很快认出来:**共享内存的组织方式,本质上就是一个多体交叉存储器(interleaved memory)。**理解了这一点,bank冲突就不再是需要死记硬背的规则,而是一个必然的推论。 ### 3.1 什么是bank 为了获得高内存带宽,共享内存被分成**32个同样大小的内存模块,称为存储体(Bank)**,它们**可以被同时访问**。这正是多体交叉存储器的思路:把一块内存拆成多个能独立、并行工作的体,只要访问落在不同的体上,就能同时进行,总带宽是单体的很多倍。官方教材给了一个很形象的比喻: > 可以把存储体比喻为一串糖葫芦。每一根穿山楂的签子对应一个内存通道(Bank),每个山楂就是一个4字节的存储单元。 关键在于地址是**如何映射到bank的**——这就是“交叉”二字的含义: > **共享内存是一个一维地址空间,以4字节为单位,地址【交错】分布在32个bank上。**也就是说:第0个4字节 → bank0,第1个 → bank1,……第31个 → bank31,第32个又回到bank0。 用组成原理的语言说,这是典型的**低位交叉编址**:地址的低位用来选bank,所以**连续的地址会被均匀打散到各个体上**。物理上,同一个bank里相邻的两个存储单元,在地址上正好相隔32(个单位)。 ![attachmentId-65e96839338c480c89bd50501dea45a7.png](/api-forum/file/RedirectImage?path=20260920/1789907489165_65e96839338c480c89bd50501dea45a7.png) *图3-8 二维共享内存示意图(数字为元素的线性下标,列为bank)* ![attachmentId-ff3cb43f305b457db765966dfed34dc0.png](/api-forum/file/RedirectImage?path=20260920/1789907497812_ff3cb43f305b457db765966dfed34dc0.png) *图3-9 四字节数据排布示意图* ### 3.2 什么是bank冲突 > **如果通过线程束发布共享内存加载或存储操作,且在每个存储体上只访问不多于一个的内存地址,那么该操作可由一个内存事务来完成(最快)。否则,该操作由多个内存事务来完成——这种情况被称为Bank冲突,会降低内存带宽的利用率。** 翻译成组成原理的话:**多体交叉存储器的并行性,只有在各个请求分散到不同存储体时才能兑现;一旦多个请求撞进同一个体,这个体只能一次服务一个,退化成串行。**有两种典型的访问模式: * **广播访问:**线程束里的多个线程访问**同一个bank的同一个地址**。这种情况硬件有优化,可以一次广播给所有线程,不算冲突。 * **串行访问:**多个线程访问**同一个bank的【不同】地址**。这就是冲突——当多个地址属于同一个bank时,必须以串行的方式逐个请求。 冲突的代价是线性叠加的。举个最坏的例子: > **如果线程束中64个线程全都访问同一存储体中的不同内存地址,那么将需要64个内存事务,消耗的时间是单一请求的64倍。** 一个原本应该“一拍完成”的访问,硬生生变成了64拍。这就是为什么bank冲突是共享内存优化里最需要盯的东西。 > **一个wavefront=64带来的细节:**DCU的线程束是64个线程,而bank只有32个。所以一个线程束的共享内存访问,本身就要分成两拍(前32线程、后32线程)来处理。分析冲突时,通常以半个线程束(32线程)为单位来看是否撞bank。 ### 3.3 一个真实的坑:按行访问 vs 按列访问 共享内存中,无论声明一维还是二维数组,实际存储都是线性排布的。两种静态声明: ```cpp __shared__ int tile[N]; __shared__ int tile[Row][Col]; ``` 现在假设我们有一块 `tile[32][32]`的二维共享内存(就是图3-8那块),有两种访问方式: ```cpp tile[threadIdx.y][threadIdx.x]; // 模式一 tile[threadIdx.x][threadIdx.y]; // 模式二 ``` 这两种写法只是把 `x`和 `y`换了个位置,看起来无伤大雅,性能却天差地别。原因在于**线程编号的增长规则**(第一部分讲过):**线程编号沿x、y、z依次递增**,所以相邻的线程,是 `threadIdx.x`相差1的线程。 ![attachmentId-53d55564204f43fea3b9195491186bd7.png](/api-forum/file/RedirectImage?path=20260920/1789907520447_53d55564204f43fea3b9195491186bd7.png) *图3-10 两种访问模式区别(绿色横框:模式一;红色竖框:模式二)* **模式一 `tile[y][x]`(图中绿色横框):**相邻线程(x相差1)访问的是 `tile[y][0]`、`tile[y][1]`、`tile[y][2]`……也就是**同一行的相邻元素**。由图3-8,同一行的相邻元素正好落在**相邻的bank**上(bank0、bank1、bank2……)。于是32个相邻线程刚好铺满32个bank,**一个冲突都没有**。这是最优的访存方式。 **模式二 `tile[x][y]`(图中红色竖框):**相邻线程(x相差1)访问的是 `tile[0][y]`、`tile[1][y]`、`tile[2][y]`……也就是**同一列的不同行元素**。由图3-8,同一列的元素(0、32、64、96……)**全都落在同一个bank**上。于是32个相邻线程全部撞进同一个bank,**退化成32路串行**,带宽掉到1/32。 一句话总结这个规律: > **让相同线程束中的线程访问不同的bank,是最优的访存方式。邻近线程在【内层维度(x)】访问相邻的单元,会因为产生更少的bank冲突而得到更好的性能。** ### 3.4 怎么规避:padding 那么模式二这种“天生要按列访问”的场景(比如矩阵转置)怎么办?**对于共享内存中的bank冲突,可通过转换全局内存和共享内存的索引来缓解。**而最常用的一招,就是第一篇提到过的**padding(填充)**。思路很直接:既然冲突来自“同一列的元素相隔32、正好同bank”,那我们就人为地把这个“32”破坏掉——把数组多开一列: ```cpp __shared__ int tile[32][33]; // 33 而不是 32 ``` 这样一来,每一行占33个单位,同一列相邻两行的元素地址就相差**33**而不是32。33和32互质,于是原本挤在一个bank里的那一列元素,被均匀地错开到了不同的bank上,冲突消失。代价仅仅是每行多浪费一个元素的空间。这一招在矩阵转置、GEMM等需要按列访问共享内存的场景里几乎是标配。第三篇讲GEMM优化时会再用到它。 ### 3.5 共享内存的价值 > **在核函数中使用共享内存,可以减少对全局内存的访问。** 把经常使用的数据先存进共享内存,利用它高带宽、低延迟的特性,当多次访问该数据时,所花的时间将远远小于访问全局内存。典型模式是: ```text 块内线程协作,把数据从【慢】的全局内存搬进【快】的共享内存(搬一次) ↓ __syncthreads() // 等所有线程都搬完 ↓ 之后所有计算都从共享内存读(读很多次) ``` **用“搬一次”换“读很多次的加速”——这就是共享内存优化的核心思想。**而bank冲突,是你在享受这份加速时必须绕开的坑。 ## **四、修饰符:告诉编译器数据放哪** ### 4.1 存储修饰符 | **修饰符** | **变量放在** | **作用域** | **生命周期** | | ---------------- | ------------------ | ---------------- | ------------------ | | (无修饰符) | 寄存器 / 本地内存 | 线程 | 线程 | | `__shared__` | 共享内存 | 线程块 | 线程块 | | `__device__` | 全局内存 | 全局(所有线程) | 整个程序 | 对应的声明写法: ```cpp float var; // 无修饰符 → 寄存器(或溢出到本地内存) __shared__ float var; // 共享内存 __device__ float var; // 全局内存(静态声明) ``` 三者的作用域和生命周期,恰好对应第一部分讲的三层结构: * **寄存器 / 本地内存**的变量,仅作用于声明它的**线程**,与线程一起创建、一起消亡。 * **共享内存**的变量,作用于声明它的线程所在的**线程块**中的所有线程,与线程块一起创建、一起消亡。 * **全局内存**的变量,作用于当前程序中的**所有线程**,与整个应用程序一起创建、一起消亡。 **记住这个对应关系,你就能在写代码前就想清楚:这个数据该被谁看到、该活多久,从而选对修饰符。** ### 4.2 函数修饰符 | **限定符** | **执行位置** | **调用位置** | **备注** | | ---------------- | ------------------ | -------------------- | ------------------------------------- | | `__global__` | 设备端 | 主机端(也可设备端) | 核函数,返回类型必须为 `void`,异步 | | `__device__` | 设备端 | 仅设备端 | 不能和 `__global__`同用 | | `__host__` | 主机端 | 仅主机端 | 一般省略不写 | 几个要点: * **`__global__`:**在设备上执行、从主机调用,返回类型**必须是 `void`**,不支持可变参数,不能是类成员函数。**注意它是异步的**(第一部分强调过)。 * **`__device__`:**在设备上执行、仅能从设备调用。常用来写“给核函数调用的辅助函数”。 * **`__host__`:**在主机上执行、仅能从主机调用,通常省略不写。 * **`__device__`和 `__host__`可以一齐使用**——这样函数会**同时**在主机端和设备端编译,一份代码两处可用。这在写既要给CPU、又要给DCU用的工具函数时很方便。 ## **五、全局内存的管理** ### 5.1 查询、分配与释放 **查询**可用/总内存: ```cpp hipError_t hipMemGetInfo(size_t *free, size_t *total); // free: 当前可用内存的快照 // total: 设备上可分配的总内存 ``` **分配**全局内存: ```cpp hipError_t hipMalloc(void** ptr, size_t size); // ptr: 返回该内存的地址 // size: 要分配的内存大小(设为0则不分配,ptr返回空地址) ``` **释放**全局内存: ```cpp hipError_t hipFree(void *ptr); ``` ### 5.2 Symbol:全局显存变量 除了动态分配,还有一种在**全局**声明显存变量的方式,叫**Symbol**,用 `__device__`修饰: ```cpp __device__ float devdata; ``` 它的初始化和读回,不能直接用 `hipMemcpy`(因为 `devdata`只是一个**标识物理位置的标识符**,不是普通的设备地址),而要用专门的接口: ```cpp hipMemcpyToSymbol(devdata, &value, sizeof(float)); // 主机 → Symbol hipMemcpyFromSymbol(&value, devdata, sizeof(float)); // Symbol → 主机 ``` 如果确实需要它的地址,可以显式获取: ```cpp float *dptr = NULL; hipGetSymbolAddress((void**)&dptr, devdata); hipMemcpy(dptr, &value, sizeof(float), hipMemcpyHostToDevice); ``` Symbol适合那些**全局唯一、贯穿整个程序**的显存变量(比如某个常量表、某个全局计数器)。 ## **六、页锁定内存:让传输更快的关键** ### 6.1 可分页内存 vs 页锁定内存 主机内存(被CPU访问的内存)分为两种: * **可分页内存(pageable memory):**可能会被操作系统“页换出”(page out),即按系统要求把虚拟内存上的数据移到别处。**第一部分默认分配的 `malloc`内存就是这种。** * **页锁定内存(page-locked / pinned memory):**被“固定”住,不会被页换出,地址是稳定的。![](https://i-blog.csdnimg.cn/direct/11a668c07c0c44f09c6e8bafbdc104dc.png)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动") ![attachmentId-Redirect1.png](/api-forum/file/RedirectImage?path=20260920/1789907606684_Redirect1.png) *图3-18 分页数据传输(左)和固定数据传输(右)* **从可分页内存传输(图左):**因为可分页内存的地址随时可能变,DMA不敢直接用它。于是主机要先**分配一块临时的固定内存,把数据从可分页内存复制过去,再从这块固定内存传给设备**。多了一次主机内部的拷贝。**从页锁定内存传输(图右):**地址是固定的,DCU知道地址后可以用DMA**直接访问**,省去了中间那次拷贝。结论: > **如果你需要频繁在主机端和设备端传输数据,使用页锁定内存对性能提升较大。** ### 6.2 页锁定内存的分配与释放 ```cpp hipError_t hipHostMalloc(void **ptr, size_t size, unsigned int flags); // 分配 hipError_t hipHostFree(void* ptr); // 释放 ``` 注意释放页锁定内存**必须**用 `hipHostFree`,不能用普通的 `free`。除了直接分配,你也可以把一块**已经分配好的可分页内存**注册成页锁定的: ```cpp hipError_t hipHostRegister(void *hostPtr, size_t sizeBytes, unsigned int flags); // 注册 hipError_t hipHostUnregister(void *hostPtr); // 取消注册 ``` ### 6.3 三种特殊的锁页内存 | **flags** | **类型** | **用途** | | ----------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `hipHostAllocPortable` | 可共享锁页内存 | 允许被多个CPU线程访问,减少线程间的数据传输和通信(普通锁页内存只能由分配它的线程访问) | | `hipHostAllocWriteCombined` | 写结合锁页内存 | CPU不再缓存这块内存、也不通过PCI-E监视它,从而**提高主机→设备的传输带宽**。适合“CPU只写、DCU来读”的数据 | | `hipHostAllocMapped` | 映射锁页内存(零拷贝内存) | 主机和设备**共享**这块内存,核函数里可以**直接访问,不需要显式拷贝** | 这里解释一下**写结合内存**为什么能提速:一般情况下,CPU操作一块内存时,会把数据缓存到Cache并监视它是否被改动,以保证缓存一致性。但在异构计算中,要算的数据大多是给DCU用的,CPU根本不需要读它——那么这套缓存和监视就是纯开销。写结合内存告诉CPU“别缓存、别监视这块”,从而省下开销、提高传输带宽。 ## **七、数据传输:同步与异步** ### 7.1 同步传输 ```cpp hipError_t hipMemcpy(void *dst, const void *src, size_t sizeBytes, hipMemcpyKind kind); ``` **同步**的含义:调用它时,**CPU端会被阻塞**,一直等到拷贝完成才执行下一步。`kind`指定方向,有四种: | **kind** | **方向** | | --------------------------- | -------------- | | `hipMemcpyHostToHost` | 主机 → 主机 | | `hipMemcpyHostToDevice` | 主机 → 设备 | | `hipMemcpyDeviceToHost` | 设备 → 主机 | | `hipMemcpyDeviceToDevice` | 设备 → 设备 | 两个坑要注意: * **`dst`和 `src`不能重叠。** * 如果指针方向和 `kind`指定的方向**不一致**,`hipMemcpy`的行为是**未定义的**——这是很隐蔽的bug来源。 ### 7.2 异步传输 ```cpp hipError_t hipMemcpyAsync(void* dst, const void* src, size_t sizeBytes, hipMemcpyKind kind, hipStream_t stream); ``` **异步**的含义:调用它时,**CPU端不受影响,控制权立即返回主机**;拷贝过程交给DMA管理。注意它多了第五个参数**`stream`(流)**。异步拷贝**需要配合流来使用**。要先创建一个非空流: ```cpp hipError_t hipStreamCreate(hipStream_t* stream); ``` 之后这个流就可以作为参数供 `hipMemcpyAsync`等异步API使用。**异步传输是实现“计算与传输重叠”的基础**——这正是第一部分那张流程图里“进阶路径”要走的路,完整机制会在下一部分《并发执行》里详细讲。这里先埋下一个引子:有了页锁定内存(第六节)+ 异步传输 + 流,你就能让“搬数据”和“算数据”同时进行,把PCI-E传输的时间藏掉。 ### 7.3 一个测带宽的小例子 ```cpp hipEventRecord(start); hipMemcpy(d_x, x, N * sizeof(float), hipMemcpyHostToDevice); hipEventRecord(stop); // ... 计算 start 到 stop 的耗时 milliseconds ... printf("Effective Bandwidth (GB/s): %f\n", N * 4 / milliseconds / 1e6); ``` 这个“有效带宽 = 传输字节数 ÷ 耗时”的思路,是性能分析的基本功——第五部分讲性能分析、第三篇讲decode优化时,我们都会反复用到它。 ## **八、数据布局:AoS还是SoA** 最后讲一个和“合并访存”密切相关的话题——数据的组织方式。它决定了你的全局内存访问效率。 存储结构化数据时,有两种组织方式: * **AoS(Array of Structure,结构体数组):**先定义结构体,再开一个结构体的数组。 * **SoA(Structure of Array,数组结构体):**每个字段各自成一个数组。 以“成对的整型数据(x, y)”为例: ```cpp // AoS:结构体数组 struct inStruct { int x; int y; }; struct inStruct myAoS[N]; // 内存里:x y x y x y x y ... // SoA:数组结构体 struct inArray { int x[N]; int y[N]; }; struct inArray mySoA; // 内存里:x x x x ... y y y y ... ``` *图3-21 AoS和SoA内存布局* **为什么这对DCU很关键?**假设有一个应用只需要处理 `x`字段,让线程t0、t1、t2、t3分别去取自己的x: * **AoS布局**(`x y x y ...`):线程们要取的x之间**隔着y**,不连续。DCU取数据时,会把夹在中间的y也**隐式地一起加载**进来——这既浪费了带宽,又浪费了缓存空间。 * **SoA布局**(`x x x x ...`):线程们要取的x**紧挨在一起**。DCU可以把这一串连续的x用一次合并访存全部取回,**带宽被充分利用**。 结论: > **在DCU上,SoA内存布局提供了合并内存访问,可以对全局内存实现更高效的利用。** 这和CPU的直觉正好相反:AoS在CPU上通常有良好的缓存局部性(一个对象的x、y挨在一起,一次缓存行就都拿到了),所以CPU程序常用AoS。但到了DCU上,因为是“很多线程各取同一字段”,**SoA往往才是更优的选择**。这是从CPU迁移过来时一个需要重新校准的直觉。 ## **小结** 本文把“数据放在哪、怎么放、怎么搬”讲了一遍,核心要点: **第一,存储分可编程和不可编程。**寄存器、共享内存、全局内存是你能控制的;L1/L2缓存是硬件自动管的。多出来的这份控制权,是DCU高性能的来源,也是它难写的原因。 **第二,四种存储各有脾气。**寄存器最快但会溢出;全局内存最大最慢;共享内存是“可编程的缓存”,快且可控;本地内存名字骗人,物理上就是全局内存,是寄存器溢出的去处。 **第三,共享内存就是一个多体交叉存储器。**它按4字节交叉分布在32个bank上。访问落在不同bank上时并行(最快),多个访问撞进同一bank时串行(bank冲突,最坏1/32带宽)。`tile[y][x]`按行访问无冲突,`tile[x][y]`按列访问全冲突;用padding(多开一列)可以错开bank化解冲突。 **第四,传输性能藏在两个细节里。**用**页锁定内存**省掉中间拷贝、让DMA直接访问;用**异步传输 + 流**让计算与传输重叠。而**数据布局用SoA**能换来合并访存——这和CPU偏爱AoS的直觉相反。 ## **参考文献** * [DCU编程与优化基础系列——CSDN博客](https://blog.csdn.net/Sakurahello1/article/details/162946708?sharetype=blogdetail&sharerId=162946708&sharerefer=PC&sharesource=Sakurahello1&spm=1011.2480.3001.8118 "DCU编程与优化基础系列——CSDN博客") * [海光DCU官方技术文档——SourceFind开发者平台](https://developer.sourcefind.cn/document/a2e964e8-a67d-11f0-909a-b6dd5499a79e?id=e309f137-ad86-11f0-846a-2e5fc4af73a0&title=3.1+%E7%BC%96%E5%86%99%E7%AC%AC%E4%B8%80%E4%B8%AADCU%E7%A8%8B%E5%BA%8F&version=a2e964e8-a67d-11f0-909a-b6dd5499a79e "海光DCU官方技术文档——SourceFind开发者平台")
全部评论(0)
只看楼主
暂无数据
--
楼主
Level.3
0 帖子
楼主热帖
暂无数据