【2023CANN训练营第二季】——通过一份入门级算子开发代码了解Ascend C算子开发流程

news2024/11/17 3:33:14

本次博客讲解的代码是Gitee代码仓的Ascend C加法算子开发代码,代码地址为:
quick-start

打开Add文件,可以看到文件结构如下:

image.png

其中add_custom.cpp是算子开发的核心文件,包括了核函数的实现,展示了如何在Ascend平台上使用Ascend C编写算子以及如何在CPU和NPU上运行算子。
main.cpp的作用是用于调用名为 add_custom 的算子进行向量相加操作,根据定义的宏 CCE_KT_TEST 来选择执行哪个部分。
CMakeLists.txt是编译cpu侧或npu侧运行的算子的编译工程文件
run.sh 编译运行算子的脚本

下面我们重点看add_custom.cpp文件和main.cpp文件,以此了解核函数的开发流程和进行CPU侧和NPU侧验证
在进行核函数开发之间,我们先分析算子的表达式:

1.算子分析

Add算子的算子分析如下:

数学表达式:
Add算子的数学表达式为 z = x + y,即将输入x和y逐元素相加,得到输出z。

输入和输出:

算子有两个输入:x和y。

输入数据类型为half(float16)。

输入数据形状(shape)为(8, 2048),即一个8x2048的二维数组。

输入数据格式(format)为ND(N-Dimensional),表示通用的多维数组格式。

算子有一个输出:z。

输出数据类型与输入数据类型相同,为half(float16)。

输出数据形状与输入形状相同,为(8, 2048)。

核函数名称和参数:

核函数的名称为 add_custom。
核函数有三个参数:x、y、z。
x和y是输入在全局内存(Global Memory)上的内存地址。
z是输出在全局内存上的内存地址。
实现所需接口:

数据搬移接口:需要使用 DataCopy 来实现输入数据的搬运,将数据从全局内存搬移到AI Core的局部内存。
矢量计算接口:通过使用矢量双目指令接口 Add 来完成x和y的逐元素相加。
内存管理接口:使用 AllocTensor 和 FreeTensor 来申请和释放Tensor数据结构,用于存储中间变量和计算结果。
队列管理接口:通过队列管理接口 EnQue 和 DeQue 来进行并行流水任务之间的通信和同步。
计算逻辑:

Add算子采用一种分块计算策略,用于在AI Core上执行向量加法操作。主要的计算逻辑包括以下步骤:
初始化核函数 add_custom,其中核函数初始化包括获取每个核的起始索引和初始化队列等。
执行计算过程,计算过程由多个循环组成,每次循环处理一个小块数据。
在每个循环中,分为以下三个步骤:
数据拷贝(CopyIn):将输入数据x和y从全局内存搬移到本地队列(Local Queue)。
计算(Compute):执行向量加法操作,将x和y逐元素相加得到z。
数据拷贝(CopyOut):将计算得到的z从本地队列搬回到全局内存。
循环处理完所有小块数据后,完成整个向量相加操作。

2.核函数开发

2.1核函数定义

这个核函数的主要作用是创建 KernelAdd 类对象,初始化并执行向量相加的计算过程,调用 Process 方法来实际执行向量相加的操作。

extern "C" __global__ __aicore__ void add_custom(GM_ADDR x, GM_ADDR y, GM_ADDR z)
{
    KernelAdd op;
    op.Init(x, y, z);
    op.Process();
}

extern “C”: 这是一个C++编译指示,用于告诉编译器要使用C链接规范,以便在C/C++混编时,能够正确链接核函数。

__global__: 这是一个GPU编程的关键字,表示核函数可以在GPU上执行。这个关键字通常用于CUDA编程,表明这是一个全局函数,可以在GPU上调用。

__aicore__: 这是针对AI Core的编程关键字,表示这是AI Core上的核函数,用于AI Core的编程模型。

void add_custom(GM_ADDR x, GM_ADDR y, GM_ADDR z): 这是核函数的定义,它接受三个参数 x、y 和 z,分别代表输入向量 x、输入向量 y 和输出向量 z 的内存地址。这些内存地址是在全局内存中的,而不是在核函数的局部内存中。

KernelAdd op;: 在核函数的开头,创建了一个名为 op 的 KernelAdd 类的对象,用于执行向量相加的计算。

op.Init(x, y, z);: 调用 KernelAdd 类的 Init 方法,初始化 op 对象,将输入向量 x 和 y 的内存地址传递给 op 对象,以及将输出向量 z 的内存地址传递给 op 对象。

op.Process();: 调用 KernelAdd 类的 Process 方法,执行向量相加的计算。这一步会根据Add算子的计算逻辑,将输入数据从全局内存拷贝到本地队列,执行向量加法操作,再将计算结果从本地队列拷贝回全局内存。

2.2算子类定义

KernelAdd 类是用于执行Add算子计算的核心类。

class KernelAdd {
public:
    __aicore__ inline KernelAdd() {}
    __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z)
    {
        // get start index for current core, core parallel
        xGm.SetGlobalBuffer((__gm__ half*)x + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
        yGm.SetGlobalBuffer((__gm__ half*)y + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
        zGm.SetGlobalBuffer((__gm__ half*)z + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);
        // pipe alloc memory to queue, the unit is Bytes
        pipe.InitBuffer(inQueueX, BUFFER_NUM, TILE_LENGTH * sizeof(half));
        pipe.InitBuffer(inQueueY, BUFFER_NUM, TILE_LENGTH * sizeof(half));
        pipe.InitBuffer(outQueueZ, BUFFER_NUM, TILE_LENGTH * sizeof(half));
    }
    __aicore__ inline void Process()
    {
        // loop count need to be doubled, due to double buffer
        constexpr int32_t loopCount = TILE_NUM * BUFFER_NUM;
        // tiling strategy, pipeline parallel
        for (int32_t i = 0; i < loopCount; i++) {
            CopyIn(i);
            Compute(i);
            CopyOut(i);
        }
    }
Init方法

Init 方法通过 GM_ADDR 类型的参数 x、y 和 z,将输入向量和输出向量的地址传入该方法。

首先,通过 GetBlockIdx() 方法获取当前AI Core的起始索引,这是为了在AI Core并行处理中计算每个核心需要处理的数据范围。

接下来,使用 xGm、yGm 和 zGm 对象,通过 SetGlobalBuffer 方法将输入向量 x、y 和输出向量 z 与AI Core的局部内存进行关联。这确保了每个核心的计算都在局部内存中进行,提高了计算效率。

然后,通过 pipe 对象,使用 InitBuffer 方法初始化了 inQueueX、inQueueY 和 outQueueZ 队列,这些队列将用于数据的输入和输出。BUFFER_NUM 和 TILE_LENGTH 用于确定队列的深度和每个队列的大小。

Process方法

Process 方法用于执行Add算子的计算逻辑。在该方法中:

通过循环处理数据,loopCount 表示循环的次数。TILE_NUM 和 BUFFER_NUM 的乘积决定了总共有多少次循环。因为采用了双缓冲策略,所以需要循环两次。

CopyIn 方法用于将数据从全局内存拷贝到本地队列,执行输入操作。

Compute 方法执行Add算子的计算,将数据从 inQueueX 和 inQueueY 队列中取出,执行相加操作。

CopyOut 方法用于将计算结果从本地队列拷贝回全局内存,执行输出操作。


CopyIn函数实现

__aicore__ inline void CopyIn(int32_t progress)
    {
        // alloc tensor from queue memory
        LocalTensor<half> xLocal = inQueueX.AllocTensor<half>();
        LocalTensor<half> yLocal = inQueueY.AllocTensor<half>();
        // copy progress_th tile from global tensor to local tensor
        DataCopy(xLocal, xGm[progress * TILE_LENGTH], TILE_LENGTH);
        DataCopy(yLocal, yGm[progress * TILE_LENGTH], TILE_LENGTH);
        // enque input tensors to VECIN queue
        inQueueX.EnQue(xLocal);
        inQueueY.EnQue(yLocal);
    }

它用于将数据从全局内存(Global Memory)复制到局部内存(Local Memory)并将数据放入输入队列,为Add算子的计算做准备。以下是对该方法的解释:

CopyIn 方法接受一个整数参数 progress,表示当前执行的迭代进度。这个参数在循环中用于确定从全局内存复制的数据位置。

首先,使用 inQueueX 和 inQueueY 队列的 AllocTensor 方法,为每个输入数据创建一个 LocalTensor 对象 xLocal 和 yLocal。这些 LocalTensor 对象用于在局部内存中存储全局内存中的部分数据。

接下来,使用 DataCopy 方法,将全局内存中的数据从 xGm 和 yGm 复制到 xLocal 和 yLocal 中。这里 progress * TILE_LENGTH 用于确定要复制的全局内存数据的位置。

然后,使用 EnQue 方法,将 xLocal 和 yLocal 放入输入队列 inQueueX 和 inQueueY 中,以便后续的计算操作可以从这些队列中获取数据。

Compute函数实现

__aicore__ inline void Compute(int32_t progress)
    {
        // deque input tensors from VECIN queue
        LocalTensor<half> xLocal = inQueueX.DeQue<half>();
        LocalTensor<half> yLocal = inQueueY.DeQue<half>();
        LocalTensor<half> zLocal = outQueueZ.AllocTensor<half>();
        // call Add instr for computation
        Add(zLocal, xLocal, yLocal, TILE_LENGTH);
        // enque the output tensor to VECOUT queue
        outQueueZ.EnQue<half>(zLocal);
        // free input tensors for reuse
        inQueueX.FreeTensor(xLocal);
        inQueueY.FreeTensor(yLocal);
    }

Compute 方法接受一个整数参数 progress,表示当前执行的迭代进度。这个参数在循环中用于确定从输入队列中获取数据以及将结果放入输出队列的位置。

首先,使用 inQueueX 和 inQueueY 队列的 DeQue 方法,从输入队列中获取 LocalTensor 对象 xLocal 和 yLocal,这些对象包含了之前在 CopyIn 方法中准备好的输入数据。

接下来,使用 outQueueZ 队列的 AllocTensor 方法,创建一个 LocalTensor 对象 zLocal,用于存储计算结果。

然后,使用 Add 方法,对 xLocal 和 yLocal 中的数据执行加法操作,将结果存储在 zLocal 中。TILE_LENGTH 参数表示每次计算的元素数量。

使用 outQueueZ 队列的 EnQue 方法,将计算结果 zLocal 放入输出队列中,以便后续的步骤可以从输出队列中获取计算结果。

最后,使用 inQueueX 和 inQueueY 队列的 FreeTensor 方法,释放 xLocal 和 yLocal 对象,以便它们可以在后续的迭代中被重用。

CopyOut函数实现

__aicore__ inline void CopyOut(int32_t progress)
    {
        // deque output tensor from VECOUT queue
        LocalTensor<half> zLocal = outQueueZ.DeQue<half>();
        // copy progress_th tile from local tensor to global tensor
        DataCopy(zGm[progress * TILE_LENGTH], zLocal, TILE_LENGTH);
        // free output tensor for reuse
        outQueueZ.FreeTensor(zLocal);
    }

首先,从 outQueueZ 队列中出队(DeQue)一个 LocalTensor 对象 zLocal,这是之前计算的结果存储在本地内存中的对象。

然后,使用 DataCopy 函数将 zLocal 中的数据复制到全局内存中的 zGm 中,复制的数据长度为 TILE_LENGTH。

最后,通过 outQueueZ.FreeTensor(zLocal) 释放 zLocal 对象,以便在下一个迭代中重新使用。

3.核函数运行验证

通过对__CCE_KT_TEST__宏定义的判断来区分CPU和NPU侧的运行程序。

3.1CPU侧运行验证

完成算子核函数CPU侧运行验证的步骤如下:

分配共享内存,并进行数据初始化;
调用ICPU_RUN_KF调测宏,完成核函数CPU侧的调用;
释放申请的资源。

#ifdef __CCE_KT_TEST__
    uint8_t* x = (uint8_t*)AscendC::GmAlloc(inputByteSize);
    uint8_t* y = (uint8_t*)AscendC::GmAlloc(inputByteSize);
    uint8_t* z = (uint8_t*)AscendC::GmAlloc(outputByteSize);

    ReadFile("./input/input_x.bin", inputByteSize, x, inputByteSize);
    ReadFile("./input/input_y.bin", inputByteSize, y, inputByteSize);

    AscendC::SetKernelMode(KernelMode::AIV_MODE);
    ICPU_RUN_KF(add_custom, blockDim, x, y, z); // use this macro for cpu debug

    WriteFile("./output/output_z.bin", z, outputByteSize);

    AscendC::GmFree((void *)x);
    AscendC::GmFree((void *)y);
    AscendC::GmFree((void *)z);

内存分配:首先,分配了三块内存,x、y 和 z,这些内存用于存储输入数据和输出数据。这些内存分配使用 AscendC::GmAlloc 函数。

数据读取:使用 ReadFile 函数,从外部文件(如 “./input/input_x.bin” 和 “./input/input_y.bin”)读取输入数据(x 和 y)。

核函数模式设置:调用 AscendC::SetKernelMode 函数,将核函数执行模式设置为 KernelMode::AIV_MODE。这表明代码将在AI Core上执行。

核函数运行:通过宏 ICPU_RUN_KF 来运行核函数,add_custom 核函数将被执行。此核函数将输入数据 x 和 y 作为参数传递,并计算结果存储在 z 中。这一步是在AI Core上执行的。

结果写入文件:使用 WriteFile 函数,将计算的结果 z 写入输出文件(如 “./output/output_z.bin”),以便进一步分析和验证。

内存释放:最后,使用 AscendC::GmFree 函数,释放之前分配的内存,包括输入数据 x 和 y,以及输出数据 z。这是为了确保不会发生内存泄漏。

3.2NPU侧验证

在NPU侧验证主要分为以下步骤:
1.初始化Device设备;
2.创建Context绑定设备;
3.分配Host内存,并进行数据初始化;
4.分配Device内存,并将数据从Host上拷贝到Device上;
5.用内核调用符<<<>>>调用核函数完成指定的运算;
6.将Device上的运算结果拷贝回Host;
7.释放申请的资源。

代码如下:

#else
    CHECK_ACL(aclInit(nullptr));
    aclrtContext context;
    int32_t deviceId = 0;
    CHECK_ACL(aclrtSetDevice(deviceId));
    CHECK_ACL(aclrtCreateContext(&context, deviceId));
    aclrtStream stream = nullptr;
    CHECK_ACL(aclrtCreateStream(&stream));

    uint8_t *xHost, *yHost, *zHost;
    uint8_t *xDevice, *yDevice, *zDevice;
    CHECK_ACL(aclrtMallocHost((void**)(&xHost), inputByteSize));
    CHECK_ACL(aclrtMallocHost((void**)(&yHost), inputByteSize));
    CHECK_ACL(aclrtMallocHost((void**)(&zHost), outputByteSize));
    CHECK_ACL(aclrtMalloc((void**)&xDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
    CHECK_ACL(aclrtMalloc((void**)&yDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
    CHECK_ACL(aclrtMalloc((void**)&zDevice, outputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));

    ReadFile("./input/input_x.bin", inputByteSize, xHost, inputByteSize);
    ReadFile("./input/input_y.bin", inputByteSize, yHost, inputByteSize);
    CHECK_ACL(aclrtMemcpy(xDevice, inputByteSize, xHost, inputByteSize, ACL_MEMCPY_HOST_TO_DEVICE));
    CHECK_ACL(aclrtMemcpy(yDevice, inputByteSize, yHost, inputByteSize, ACL_MEMCPY_HOST_TO_DEVICE));

    add_custom_do(blockDim, nullptr, stream, xDevice, yDevice, zDevice);
    CHECK_ACL(aclrtSynchronizeStream(stream));

    CHECK_ACL(aclrtMemcpy(zHost, outputByteSize, zDevice, outputByteSize, ACL_MEMCPY_DEVICE_TO_HOST));
    WriteFile("./output/output_z.bin", zHost, outputByteSize);

    CHECK_ACL(aclrtFree(xDevice));
    CHECK_ACL(aclrtFree(yDevice));
    CHECK_ACL(aclrtFree(zDevice));
    CHECK_ACL(aclrtFreeHost(xHost));
    CHECK_ACL(aclrtFreeHost(yHost));
    CHECK_ACL(aclrtFreeHost(zHost));

    CHECK_ACL(aclrtDestroyStream(stream));
    CHECK_ACL(aclrtDestroyContext(context));
    CHECK_ACL(aclrtResetDevice(deviceId));
    CHECK_ACL(aclFinalize());

总结:以上就是整个Ascend C算子开发的流程,接下来就可以执行一键式编译运行脚本,编译和运行应用程序。总的来说,通过这个简单的例子,可以知道Ascend C算子开发的工作主要分为:环境准备、算子分析、核函数开发、核函数运行验证、编译运行脚本这就几个步骤,核函数开发和核函数验证运行需要重点掌握,里面涉及到了算子开发核心知识。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【PG】PostgreSQL客户端认证pg_hba.conf文件

目录 文件格式 连接类型(TYPE) 数据库&#xff08;database&#xff09; 用户(user) 连接地址&#xff08;address&#xff09; 格式 IPv4 IPv6 字符 主机名 主机名后缀 IP-address/IP-mask auth-method trust reject scram-sha-256 md5 password gss sspi …

图纸管理制度《四》

1、目的 使公司的图纸得到有效的控制&#xff0c;确保生产所用的图纸为最新有效版本&#xff0c;避免因图纸管理不当造成的损失。 2、定义 本制度所述的图纸包括产品总装图、装配图、零件图、工装图纸、检具图纸、包装图纸、工艺流程 3、范围 客户提供的图纸&#xff0c;技…

修改 jquery dialog title

官网&#xff1a; $("#xxx").dialog("option", "title", "xxx").dialog(open);

Pycharm连接远程服务器 导入远程包时本地显示红色解决方法

1、问题描述 使用pycharm工具 进行数据开发任务时&#xff0c;由于使用远程服务器进行开发的&#xff0c;代码在远程服务器上执行&#xff0c;导入远程服务器代码时&#xff0c;在本地数据脚本显示标准为红色&#xff0c;import XXX 显示红色问题。 2、问题分析&#xff1a; 是…

不同设备的请求头信息UserAgent,Headers

一、电脑端 【设备名称】&#xff1a;电脑 Win10 【应用名称】&#xff1a;win10 Edge 【浏览器信息】&#xff1a;名称:(Chrome)&#xff1b;版本:(70.0) 【请求头信息】&#xff1a;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Ch…

PS笔记2_钢笔工具的形状和路径

本文目录 前言Step 1 形状的用法&#xff1a;画图Step 2 路径的用法&#xff1a;抠图 前言 当我们在PS中选择钢笔工具时&#xff0c;上方功能栏中可以选择钢笔的功能项&#xff0c;有三种选项&#xff1a;形状&#xff0c;路径和像素。最常用的就是“形状”和“路径”。本博文…

Java 基础面试题,JVM 内存模型?

我们在 Java 岗位的面试题中&#xff0c;大概率会碰到这样一个面试题&#xff1a;请你解释你对 JVM 内存模型的理解。 今天我们就来回答一下这个问题&#xff1a; JDK 11 中的 JVM 内存模型可以分为以下几个部分&#xff1a; 程序计数器&#xff08;Program Counter&#xff…

html截取最后几个字符

html截取最后几个字符 string str"123abc456"; int i3; 1 取字符串的前i个字符 strstr.Substring(0,i); // or strstr.Remove(i,str.Length-i); 2 去掉字符串的前i个字符&#xff1a; strstr.Remove(0,i); // or strstr.Substring(i); 3 从右边开始取i个字符&…

软件开发最近很吃香,嵌入式建议转行吗?

今日话题&#xff0c;软件开发最近很吃香&#xff0c;嵌入式建议转行吗&#xff1f;软件开发和嵌入式领域各有优势&#xff0c;是否要转行需要综合考虑。嵌入式领域的薪资水平受方向和行业的影响较大。在做出决策前&#xff0c;务必进行充分调研&#xff0c;不要被互联网的繁荣…

浅谈安科瑞无线测温产品在荷兰某配电室项目中的应用

摘要&#xff1a;随着电力工业的发展&#xff0c;对设备的安全性、可靠性要求越来越高。在这种条件下&#xff0c;高压设备的无线测温系统应运而生。这种技术是将内置电池或电流感应和无线发射模块的测温传感器安装于各测温点&#xff0c;由于其体积小&#xff0c;且无需任何接…

华为数通方向HCIP-DataCom H12-831题库(多选题:101-120)

第101题 LSR对收到的标签进行保留,且保留方式有多种,那么以下关于LDP标签保留一自由方式的说法 A、保留邻居发送来的所有标签 B、需要更多的内存和标签空间 C、只保留来自下一跳邻居的标签,丢弃所有非下一跳铃邻居发来的标签 D、节省内存和标签空间 E、当IP路由收敛、下一跳…

从入门到精通:深入了解CSS中的Grid网格布局技巧和应用!

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! ​ 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一…

鸿蒙应用开发之数据管理

一、概述 在移动互联网蓬勃发展的今天&#xff0c;移动应用给我们生活带来了极大的便利&#xff0c;这些便利的本质在于数据的互联互通。因此在应用的开发中数据存储占据了非常重要的位置&#xff0c;HarmonyOS应用开发也不例外。 本文将为您介绍HarmonyOS提供的数据管理能力之…

酒店宾馆在线订房小程序源码系统:轻松预订 出行无忧 带完整搭建教程

大家好啊&#xff0c;罗峰来给大家推荐一款酒店宾馆在线订房小程序源码系统&#xff0c;随着互联网技术的发展和普及&#xff0c;越来越多的人选择在线预订酒店宾馆。为了满足这一需求&#xff0c;各大酒店订房APP或是小程序层出不穷&#xff0c;而搭建一个酒店宾馆在线订房小程…

SQL server 代理服务启动和查看

设置重启 使用管理员权限登录到运行 SQL Server 代理服务的计算机。 打开 Windows 服务管理器。可以通过按下 Windows 键 R&#xff0c;然后键入 "services.msc" 并按 Enter 来打开服务管理器。 在服务列表中&#xff0c;找到 "SQL Server Agent" 服务&…

这可能是你见过的最NB的C++课程【WGL视频笔记 思考总结】

继承 为什么使用继承&#xff1f; 代码重用。 代码演示&#xff1a; #include <iostream> #include <string>using namespace std;class Human { public:void eat(string food){cout << food << endl;} };class Student: public Human { public:v…

LUCEDA IPKISS------Definition Properties 表格查询

LUCEDA IPKISS------Definition Properties 表格查询

下载视频号安装,下载视频号安装到手机上?

在数字化时代&#xff0c;随着社交媒体的蓬勃发展&#xff0c;视频内容正成为品牌传播和用户吸引的重要方式。而作为当下最热门的短视频平台之一&#xff0c;视频号为用户提供了创作、分享和推广优质内容的机会。如果您还不了解视频号视频或想进一步了解如何下载视频号视频&…

双11便宜云服务器有哪些值得推荐的

本次2023双11云服务器各大厂商活动终于是开启了&#xff0c;其中最受人关注的莫过于阿里云和腾讯云两家大厂商&#xff0c;不过貌似也与往常一样&#xff0c;始终是对老用户不太友好。 那么除了阿里云/腾讯云/华为云等一系列大厂商外&#xff0c;还有一些厂商是比较值得关注的&…

字符串中strcmp和strncmp的比较

strcmp&#xff1a;函数原型是int strcmp(const char *s1,const char *s2)&#xff0c;功能如下&#xff1a;若strlstr2&#xff0c;则返回0;若strl<str2&#xff0c;则返回-1;若strl>str2&#xff0c;则返回1。 strncmp&#xff1a;函数原型是int strncmp( const char …