SIMD性能优化

news2024/11/14 18:19:05

文章目录

    • 前言
      • MMX
      • SSE
      • AVX
    • 使用
      • 内置函数使用
        • SSE/AVX命名规则
        • SSE/AVX操作类别
        • 实战
    • 汇编使用
      • 优化前代码详解
      • 优化后代码详解
    • 引用文章

编码性能优化大法
算法优化
硬件加速
多线程并行
算法自适应升降级
冗余计算去除
箅法裁剪
以空间换时间
CPU加速
GPU加速
帧内多线程
帧间多线程
CPU大小核绑定
SIMD加速
cache miss优化
编译优化
GPU shader
GPU memory zero-copy

前言

SIMD(Single Instruction Multiple Data)是CPU硬件层面支持的用于对数据进行并行操作。

原理:采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术

它的指令集存在如下:

  • X86下的实现为MMXSSEAVX指令集

  • ARM下的实现为NEON指令集

MMX

1996年Intel推出了X86的MMX(MultiMedia eXtension)指令集

  • MMX定义了8个64位寄存器(MM0-MM7),以及相应的操作指令
  • 可用于以“压缩”格式保存64位整数或多个较小整数,并没有浮点数的支持!

注意:上面说的是x86的指令扩展,可以看到有点类似于64兼容32操作系统架构"rdi->edi"。

2003年才以 x86-64 和 64 位 PowerPC 处理器架构的形式引入到(在此之前是 32 位)个人计算机领域的主流。

SSE

1999年推出了全面覆盖MMX的SSE(Streaming SIMD Extensions)流式SIMD扩展指令集

  • 添加了8个新的128位寄存器(XMM0-XMM7)
  • 开始支持单个寄存器存储4个32单精度浮点数

X86-64架构世界的到来:

  • 在原来的基础上添加了8个寄存器(XMM8至XMM15)
  • 支持单个寄存器存储2个64双精度浮点数

AVX

2011年推出了延伸SSE的AVX(Advanced Vector Extensions)高级向量扩展指令集

  • 引入了16个256位寄存器(YMM0-YMM15)
  • AVX的256位寄存器和SSE的128位寄存器存在着相互重叠的关系(XMM寄存器为YMM寄存器的低位)

最好不要混用AVX与SSE指令集,否在会导致transition penalty(过渡处罚)

目前Apple OS X 10.6.8、Linux 2.6.30、Windows 7,可见现在AVX指令集是一个主流指令集

总结:

SIMD指令集
MMX
SSE
AVX
8个64位寄存器(MM0-MM7)
8个新的128位寄存器(XMM0-XMM7)
添加了8个寄存器(XMM8至XMM15)
16个256位寄存器(YMM0-YMM15)

使用

实现SIMD的方法如下:

  1. 使用Intel开发的跨平台函数库(Intel IPP库)
  2. 借助于Auto-vectorization(自动矢量化),即借助编译器将标量操作转化为矢量操作
  3. 使用编译器指示符,如Cilk里的#pragma simd和OpenMP里的#pragma omp simd
  4. 使用内置函数,高级语言中类似调用普通函数一样使用simd,函数的具体实现定义在编译器中
  5. 使用汇编直接操作SIMD指令和寄存器,高级语言中嵌入汇编代码,极致的性能优化

FFmpeg对simd的使用就是“内置函数”形式
如:4.2.2中 的libavutil/x86/intmath.h:#include<immintrin.h>

4.2.2中没找到向量寄存器的使用

内置函数使用

SSE/AVX指令主要定义于以下一些头文件中:

  • <xmmintrin.h> : SSE, 支持同时对4个32位单精度浮点数的操作。
  • <emmintrin.h> : SSE 2, 支持同时对2个64位双精度浮点数的操作。
  • <pmmintrin.h> : SSE 3, 支持对SIMD寄存器的水平操作(horizontal operation),如hadd, hsub等…。
  • <tmmintrin.h> : SSSE 3, 增加了额外的instructions。
  • <smmintrin.h> : SSE 4.1, 支持点乘以及更多的整形操作。
  • <nmmintrin.h> : SSE 4.2, 增加了额外的instructions。
  • <immintrin.h> : AVX, 支持同时操作8个单精度浮点数或4个双精度浮点数。

每一个头文件都包含了之前的所有头文件,所以引用immintrin.h即可使用SSE、AVX的内在函数

SSE/AVX命名规则

数据类型通常以_mxxx(T)的方式进行命名

xxx代表数据的位数:

  • SSE提供的__m128为128位
  • AVX提供的__m256为256位

T为类型:

  • 若为单精度浮点型则省略
  • 若为整形则为i,如__m128i
  • 若为双精度浮点型则为d,如__m256d。

操作浮点数的内置函数命名方式为_mm(xxx)_name_PT

name为函数执行的操作的名字:

  • _mm_add_ps ,加法
  • _mm_sub_ps ,减法

P代表的是对矢量或者标量进行操作:

  • _mm_add_ss ,只对最低位的32位浮点数执行加法
  • _mm_add_ps ,对4个32位浮点数执行加法操作

T代表浮点数的类型:

  • _mm_add_pd, d则为双精度浮点
  • _mm_add_ps, s则为单精度浮点型

操作整形的内置函数命名方式为:_mm(xxx)_name_epUY

U为整数的类型:

  • _mm_adds_epu16 , u为无符号类型
  • _mm_adds_epi16 , i为有符号类型

Y为操作的数据类型的位数:

  • _mm_cvtpd_pi32

SSE/AVX操作类别

存取操作(load/store/set)

  • load系列可以用来从内存中载入数据到SSE/AVX提供的类型中
  • store系列可以将SSE/AVX提供的类型中的数据存储到内存中
  • set系列可以直接设置SSE/AVX提供的类型中的数据

算术运算(常用部分)

  • _mm_add_ps,_mm_add_ss等加法系列
  • _mm_sub_ps,_mm_sub_pd等减法系列
  • _mm_mul_ps,_mm_mul_epi32等乘法系列
  • _mm_div_ps,_mm_div_ss等除法系列
  • _mm_sqrt_pd,_mm_rsqrt_ps等开平方系列
  • _mm_rcp_ps,_mm_rcp_ss等求倒数系列
  • _mm_dp_pd,_mm_dp_ps计算点乘

比较运算(常用部分)

  • _mm_max_ps逐分量对比两个数据,并将较大的分量存储到返回类型的对应位置中。
  • _mm_min_ps逐分量对比两个数据,并将较小的分量存储到返回类型的对应位置中。
  • _mm_cmpeq_ps逐分量对比两个数据是否相等。
  • _mm_cmpge_ps逐分量对比一个数据是否大于等于另一个是否相等。
  • _mm_cmpgt_ps逐分量对比一个数据是否大于另一个是否相等。
  • _mm_cmple_ps逐分量对比一个数据是否小于等于另一个是否相等。
  • _mm_cmplt_ps逐分量对比一个数据是否小于另一个是否相等。
  • _mm_cmpneq_ps逐分量对比一个数据是否不等于另一个是否相等。
  • _mm_cmpnge_ps逐分量对比一个数据是否不大于等于另一个是否相等。
  • _mm_cmpngt_ps逐分量对比一个数据是否不大于另一个是否相等。
  • _mm_cmpnle_ps逐分量对比一个数据是否不小于等于另一个是否相等。
  • _mm_cmpnlt_ps逐分量对比一个数据是否不小于另一个是否相等。

逻辑运算(常用部分)

  • _mm_and_pd对两个数据逐分量and
  • _mm_andnot_ps先对第一个数进行not,然后再对两个数据进行逐分量and
  • _mm_or_pd对两个数据逐分量or
  • _mm_xor_ps对两个数据逐分量xor

实战

以下使用宏定义方式分别运行AVX、SSE指令集:

//Building :
//- AVX Pattern "clang demo.c -D AVX -mavx && ./a.out"
//- SSE Pattern "clang demo.c && ./a.out"
#include <stdio.h>
#include <immintrin.h>
#include <sys/time.h>

#define N 170 * 1024 * 1024
#define SEED 0x100

int main(){
#if defined(AVX)
	//AVX
	float* a = (float*) _mm_malloc(N * sizeof(float), 32);
	float* b = (float*) _mm_malloc(N * sizeof(float), 32);
	float* c = (float*) _mm_malloc(N * sizeof(float), 32);
#else
	//SSE
	float* a = (float*) _mm_malloc(N * sizeof(float), 16);
	float* b = (float*) _mm_malloc(N * sizeof(float), 16);
	float* c = (float*) _mm_malloc(N * sizeof(float), 16);
#endif

	srand(SEED);
	for (int i = 0; i < N; i++) {
		a[i] = b[i] = (float)(rand() % N);
	}

	struct timeval before, after;

	gettimeofday(&before, NULL);
	//====================begin times====================
	int i = 0;
#if defined(AVX)
	//AVX
	__m256 A,B,C; // 向量类型 __m256 = 8xfloat
	for (; i < (N & (~(unsigned)7)); i+=8) {
		A = _mm256_load_ps(&a[i]);  //256bit = 32byte 表示并行操作32byte数据
		B = _mm256_load_ps(&b[i]);
    //将压缩的单精度浮点值从对齐的内存位置移动到目标向量。对应的英特尔®avx指令为 VMOVAPS
		C = _mm256_mul_ps(A,B);
    //将浮点数与32个向量相乘。对应的英特尔®avx指令为 VMULPS
		_mm256_store_ps(&c[i],C);
    //将打包的单精度浮点值从float32向量移动到对齐的内存位置。相应的英特尔®AVX指令是VMOVAPS。  即__m256 C 移动到 c指针的位置
	}
#else
	//SSE
	__m128 A,B,C; // 向量类型 __m128 = 4xfloat
	for (; i < (N & (~(unsigned)3)); i+=4) {
		A = _mm_load_ps(&a[i]);
		B = _mm_load_ps(&b[i]);
		C = _mm_mul_ps(A,B);
		_mm_store_ps(&c[i],C);
	}
#endif
	//====================end times====================
	gettimeofday(&after, NULL);
	printf("%f, %f, %f, %f\n", c[0], c[1], c[N-2], c[N-1]);


  double msecs = 0.0;
	msecs = (after.tv_sec - before.tv_sec)*1000.0 + (after.tv_usec - before.tv_usec)/1000.0;
#if defined(AVX)
	printf("AVX pattern execution time = %2.3lf ms\n", msecs);
#else
	printf("SSE pattern execution time = %2.3lf ms\n", msecs);
#endif

	_mm_free(c);
	_mm_free(b);
	_mm_free(a);
	return 0;
}

运行结果:

$ clang demo.c && ./a.out                                    
SSE pattern execution time = 512.333 ms

$ clang demo.c -D AVX -mavx && ./a.out                       
AVX pattern execution time = 417.597 ms

汇编使用

go的数据操作模块

var a []byte
var b []byte
for i,_ := range a{
  if a[i] != b[i]
  	return false;
}

下图是使用 SIMD 技术优化汇编代码前后的对比图:

优化前代码详解

//func Equal(a, b []byte) bool
TEXT bytes·Equal(SB),NOSPLIT,$0-49
//---------数据加载------------
    // 将栈上数据取到寄存器中
    // 对数组长度进行比较,如果不相等直接返回0
    MOVD a_len+8(FP), R1        // 取数组a的长度
    MOVD b_len+32(FP), R3      // 取数组b的长度
    CMP R1, R3                         // 数组长度比较
    BNE notequal                      // 数组长度不同,跳到notequal
    MOVD a+0(FP), R0              // 将数组a的地址加载到通用寄存器R0中
    MOVD b+24(FP), R2            // 将数组b的地址加载到通用寄存器R2中
    ADD R0, R1                         // R1保存数组a末尾的地址
//-----------------------------
//--------数组循环比较操作-------
loop:
    CMP R0, R1                         // 判断是否到了数组a末尾
    BEQ equal                           // 如果已经到了末尾,说明之前都是相等的,跳转到标签equal
    MOVBU.P 1(R0), R4             // 从数组a中取一个byte加载到通用寄存器R4中
    MOVBU.P 1(R2), R5             // 从数组b中取一个byte加载到通用寄存器R5中
    CMP R4, R5                         // 比较寄存器R4、R5中的值
    BEQ loop                             // 相等则继续下一轮循环操作
//-----------------------------
//-------------不相等-----------
notequal:
    MOVB ZR, ret+48(FP)          // 数组不相等,返回0
    RET
//-----------------------------
//-------------相等-------------
equal:
    MOVD $1, R0                       // 数组相等,返回1
    MOVB R0, ret+48(FP)
    RET
//-----------------------------

优化后代码详解

// 函数的参数,此处是通过寄存器传递参数的
// 调用memeqbody的父函数已经将参数放入了如下寄存器中
// R0: 寄存器R0保存数组a的地址
// R1: 寄存器R1数组a的末尾地址
// R2: 寄存器R2保存数组b的地址
// R8: 寄存器R8存放比较的结果
TEXT runtime·memeqbody<>(SB),NOSPLIT,$0
//---------------数组长度判断-----------------
// 根据数组长度判断按照何种分块开始处理
    CMP    $1, R1
    BEQ    one
    CMP    $16, R1
    BLO    tail
    BIC    $0x3f, R1, R3
    CBZ    R3, chunk16
    ADD    R3, R0, R6

//------------处理长度为64 bytes的块-----------
// 按64 bytes为块循环处理
chunk64_loop:
// 加载RO,R2指向的数据块到SIMD向量寄存器中,并将RO,R2指针偏移64位
    VLD1.P (R0), [V0.D2, V1.D2, V2.D2, V3.D2]
    VLD1.P (R2), [V4.D2, V5.D2, V6.D2, V7.D2]
// 使用SIMD比较指令,一条指令比较128位,即16个bytes,结果存入V8-v11寄存器
    VCMEQ  V0.D2, V4.D2, V8.D2
    VCMEQ  V1.D2, V5.D2, V9.D2
    VCMEQ  V2.D2, V6.D2, V10.D2
    VCMEQ  V3.D2, V7.D2, V11.D2
// 通过SIMD与运算指令,合并比较结果,最终保存在寄存器V8中
    VAND   V8.B16, V9.B16, V8.B16
    VAND   V8.B16, V10.B16, V8.B16
    VAND   V8.B16, V11.B16, V8.B16
// 下面指令判断是否末尾还有64bytes大小的块可继续64bytes的循环处理
// 判断是否相等,不相等则直接跳到not_equal返回
    CMP    R0, R6                             // 比较指令,比较RO和R6的值,修改寄存器标志位,对应下面的BNE指令
    VMOV   V8.D[0], R4
    VMOV   V8.D[1], R5                   // 转移V8寄存器保存的结果数据到R4,R5寄存器
    CBZ    R4, not_equal
    CBZ    R5, not_equal                   // 跳转指令,若R4,R5寄存器的bit位出现0,表示不相等,跳转not_equal
    BNE    chunk64_loop                  // 标志位不等于0,对应上面RO!=R6则跳转chunk64_loop
    AND    $0x3f, R1, R1                   // 仅保存R1末尾的后6位,这里保存的是末尾不足64bytes块的大小
    CBZ    R1, equal                         // R1为0,跳转equal,否则向下顺序执行

...............................................
...............................................

//-----------循环处理长度为16 bytes的块------------
chunk16_loop:
    VLD1.P (R0), [V0.D2]
    VLD1.P (R2), [V1.D2]
    VCMEQ    V0.D2, V1.D2, V2.D2
    CMP R0, R6
    VMOV V2.D[0], R4
    VMOV V2.D[1], R5
    CBZ R4, not_equal
    CBZ R5, not_equal
    BNE chunk16_loop
    AND $0xf, R1, R1
    CBZ R1, equal
//-----处理数组末尾长度小于16、8、4、2 bytes的块-----
tail:
    TBZ $3, R1, lt_8
    MOVD.P 8(R0), R4
    MOVD.P 8(R2), R5
    CMP R4, R5
    BNE not_equal

lt_8:
    TBZ $2, R1, lt_4
    MOVWU.P 4(R0), R4
    MOVWU.P 4(R2), R5
    CMP R4, R5
    BNE not_equal

lt_4:
    TBZ $1, R1, lt_2
    MOVHU.P 2(R0), R4
    MOVHU.P 2(R2), R5
    CMP R4, R5
    BNE not_equal

lt_2:
    TBZ     $0, R1, equal

one:
    MOVBU (R0), R4
    MOVBU (R2), R5
    CMP R4, R5
    BNE not_equal
//-----------------判断相等返回1----------------
equal:
    MOVD $1, R0
    MOVB R0, (R8)
    RET
//----------------判断不相等返回0----------------
not_equal:
    MOVB ZR, (R8)
    RET

上述优化代码中:

  • 使用 VLD1(数据加载指令)一次加载 64bytes 数据到 SIMD 寄存器
  • 再使用 VCMEQ(相等比较指令)比较 SIMD 寄存器保存的数据内容得到结果

大于 16byte 小于 64byte 块数据,使用一个 SIMD 寄存器一次处理 16byte 块的数据

小于 16byte 数据块使用通用寄存器保存数据,一次比较 8\4\2\1byte 的数据块

引用文章

  • https://juejin.cn/post/7091571543239000078
  • https://xie.infoq.cn/article/9354c2496e3652fd6560aa074
  • https://zhuanlan.zhihu.com/p/55327037
  • https://www.eet-china.com/mp/a71752.html

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

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

相关文章

【黑马学成在线2023版】解决P7创建父工程时Maven的pom文件依赖爆红问题(亲测有效)

本期目录问题描述解决方案问题描述 感谢黑马贡献的高质量视频教程《学成在线》微服务项目。笔者在学到《P7-创建父工程基础工程》时&#xff0c;直接粘贴黑马老师的 pom 文件中的依赖会出现依赖的 <artifactId> 和 <version> 爆红&#xff0c;显示无法找到依赖的错…

vite学习笔记

vite 1.vite是什么&#xff1f; 基于ES-Module的前端构建工具 2.为什么选择vite&#xff1f; 在浏览器支持 ES 模块之前&#xff0c;JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发。 缓慢的服务器启动 当冷启动开发服务器时&#xff0c;基于打包器的方式…

YOLOv8来啦!YOLO内卷期模型怎么选?9+款AI硬件如何快速部署?深度解析

在这新春佳节到来之际&#xff0c;回顾整个虎年&#xff0c;堪称YOLO内卷元年&#xff0c;各路YOLO系列神仙打架&#xff0c;各显神通。一开始大部分用户做项目做实验还是使用的YOLOv5&#xff0c;然后YOLOv6、YOLOv7、PP-YOLOE、DAMO-YOLO、RTMDet就接踵而至&#xff0c;于是就…

SpringBoot原理解析

目录 一、Profile功能 &#xff08;一&#xff09;、application-profile功能 &#xff08;二&#xff09;、Profile条件装配功能 &#xff08;三&#xff09;、profile分组 二、外部化配置 &#xff08;一&#xff09;、外部配置源 &#xff08;二&#xff09;、配置文…

消息队列 ---nsq

设计 topic和channel 单个nsqd实例旨在一次处理多个数据流。流称为“主题”&#xff0c;一个主题有 1 个或多个“通道”。每个通道都会收到一个主题的所有消息的_副本_。 主题和通道道_不是_提前配置的。主题是在首次使用时通过发布到指定主题或订阅指定主题的通道来创建的。…

如何看懂行业分析报告?

从下面几部分聊聊行业分析&#xff1a;1.什么时候需要做行业分析&#xff1f;2.如何做行业分析&#xff1f;3.案例学习4.在工作中如何应用&#xff1f;5.在生活中如何应用&#xff1f;1.什么时候需要做行业分析呢&#xff1f;当你在对自己进行职业规划的时候&#xff0c;会思考…

【SpringCloud15】SpringCloud Stream消息驱动

1.消息驱动概述 1.1 为什么要引入消息驱动 1.2 是什么 概述&#xff1a;屏蔽底层消息中间件的差异&#xff0c;降低切换成本&#xff0c;统一消息的编程模型 官网 Spring Cloud Stream是用于构建与共享消息传递系统连接的高度可伸缩的事件驱动微服务框架&#xff0c;该框架提…

一 、Qml开发之环境搭建

进入官网下载相应版本的qtcreator &#xff1a;https://download.qt.io/archive/qt/5.12/5.12.6/ 1.1 安装的时候注意如下对话框&#xff0c;需要选择下图所示的必须选项&#xff0c;因为我是mac 所以选择的macOS下载完之后进行点击安装&#xff0c;安装后运行软件图片如下&…

小程序uni-app介绍

uni-app介绍 uni-app简介 uni-app 是一个使用**Vue.js **开发所有前端应用的框架&#xff0c;开发者编写一套代码&#xff0c;可发布到iOS、Android、Web&#xff08;响应式&#xff09;、以及各种小程序&#xff08;微信/支付宝/百度/头条/QQ/钉钉/淘宝&#xff09;、快应用…

C++|读写xml文件开源库tingxml2的使用

参考&#xff1a; TinyXML使用方法[通俗易懂] https://cloud.tencent.com/developer/article/2037579 TinyXML2 入门教程&#xff08;这篇写很好&#xff0c;本文侧重讲解使用不过做多介绍&#xff09; 不了解xml的建议自行查阅&#xff0c;在此不赘述。 开源库github链接&…

Python中的列表、元组、字典

​​​​​​​列表是一种让程序员在代码中批量表示/保存数据的方式&#xff0c;元组和列表相比&#xff0c;是非常相似的&#xff0c;只是列表中放哪些元素可以修改调整&#xff0c;元组中放的元素是创建元组的时候就设定好的&#xff0c;不能修改调整。 列表和元组类似于其他…

SpringBoot 2-9-2 ServletAPI

使用27个解析器中 ServletRequestMethodArgumentResolver Step1 页面请求 注意RestController ResponseBody Controller Controller 将当前修饰的类注入SpringBoot IOC容器&#xff0c;使得从该类启动后就被实例化 ResponseBody 表示它会以Json字符串的形式返回给客户…

【日常系列】LeetCode《27·动态规划2》

数据规模->时间复杂度 <10^4 &#x1f62e;(n^2) <10^7:o(nlogn) <10^8:o(n) 10^8<:o(logn),o(1) 内容 1&#xff09;爬楼梯、打家劫舍问题 2&#xff09;0-1&#xff0c;多重&#xff0c;完全&#xff0c;二维被动背包问题 lc 70【剑指 10 - 2】【top100】&…

Maven仓库集成与使用

1.概念:Maven主要服务于基于java平台的项目构建(编译、测试、生成文档、打包、部署等)&#xff0c;依赖管理和项目信息管理。 2.四大特性: 2.1:依赖管理系统(jar包管理, jar 升级时修改配置文件即可) 依赖(Coordination):由groupId、artifactId、version组成 …

PHP MySQL 预处理语句

预处理语句对于防止 MySQL 注入是非常有用的。 预处理语句及绑定参数 预处理语句用于执行多个相同的 SQL 语句&#xff0c;并且执行效率更高。 预处理语句的工作原理如下&#xff1a; 预处理&#xff1a;创建 SQL 语句模板并发送到数据库。预留的值使用参数 "?" 标…

Python 实现 JSON 解析器

Json 解析 文章目录Json 解析Json 的组成对象结构数组结构词法分析逻辑性解析解析对象类型解析数组类型完整代码小结Json 的组成 JSON结构共有2种 对象结构数组结构 一个合法的JSON字符串可以包含这几种元素: 特殊符号,如"{" “}“表示一个JSON Object&#xff0…

将DataFrame进行转置的DataFrame.transpose()方法

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 将DataFrame进行转置 DataFrame.transpose() 选择题 关于以下python代码说法错误的一项是? import pandas as pd dfpd.DataFrame({a:[a1,a2],b:[b1,b2]}) print("【显示】df:\n"…

高德地图红绿灯读秒是怎么实现的?(一)

关于这个读秒实现功能众说风云&#xff0c;目前有两种说法&#xff0c;一种说是靠大数据分析&#xff0c;一种说是靠交管部门数据。 我们先看一下官方的回应&#xff1a;可以自行去抖音看官方号的解释。 以下为原答&#xff1a; 有人说是接入了地方交管数据&#xff0c;其实政策…

2022年度 FinClip 扩展 SDK 推荐!

2022年&#xff0c;FinClip 团队进行了24个产品迭代&#xff0c;为了丰富FinClip 的平台能力&#xff0c;除了核心SDK之外&#xff0c;我们还为开发者们提供了扩展SDK&#xff0c;扩展SDK是一个依赖核心SDK的库&#xff0c;里面提供了核心SDK中所没有的各种小程序API。 官方希…

arduino和物联网云端平台系列---物模型之事件

事件&#xff0c;先下个简单的定义就是发生了什么事件 系列文章都是已经完成了基本的库安装和使用为前提 物模型之事件 基本的添加步骤不描述了&#xff0c;设置一个测试用例 事件我已经设定好了&#xff0c;输出参数代表的是在云端得到的输出&#xff0c;需要我们在设备进行…