当前位置: 首页 > news >正文

NEON优化:性能优化经验总结

NEON优化:性能优化经验总结

  • 1. 什么是 NEON
    • Arm Adv SIMD 历史
  • 2. 寄存器
  • 3. NEON 命名方式
  • 4. 优化技巧
  • 5. 优化 NEON 代码(Armv7-A内容,但区别不大)
    • 5.1 优化 NEON 汇编代码
      • 5.1.1 Cortex-A 处理器之间的 NEON 管道差异
      • 5.1.2 内存访问优化

Reference:

  1. NEON优化:性能优化经验总结
  2. NEON官方内联函数
  3. Arm NEON programming quick reference
  4. Learn the architecture - Neon programmers’ guide

在这里插入图片描述

1. 什么是 NEON

NEON 技术是用于 Arm Cortex-A 系列处理器的先进 SIMD(单指令多数据)架构。它可以加速多媒体和信号处理算法,如视频编码器/解码器、2D/3D图形、游戏、音频和语音处理、图像处理、电话和声音。

NEON 指令执行“打包 SIMD”处理:

  • 寄存器被认为是相同数据类型元素的向量
  • 数据类型支持:带符号/无符号 8 8 8 位, 16 16 16 位, 32 32 32 位, 64 64 64 位,ARM 32 32 32位平台上的单精度浮点数,ARM 64 64 64位平台上的单精度浮点数和双精度浮点数。
  • 指令在所有通道中执行相同的操作

Arm Adv SIMD 历史

Armv6Armv7-AArmv8-A AArch64
SIMD extensionNEONNEON
在32位通用ARM寄存器上操作独立的寄存器库,32x64位NEON寄存器独立的寄存器库,32x128位NEON寄存器
8位或16位整数8/16/32/64位整数8/16/32/64位整数
每条指令进行2x16位/4x8位操作单精度浮点数单精度浮点数、双精度浮点数
每条指令最多16x8位操作(16x4吧,待确认)每条指令最多16x8位操作

2. 寄存器

Armv7-A 和 AArch32 具有相同的通用 Arm 寄存器 - 16 16 16 x 32 32 32 位通用 Arm 寄存器(R0-R15)。

Armv7-A 和 AArch32 具有 32 x 64 位 NEON 寄存器(D0-D31)。这些寄存器也可以看作是 16 × 128 位寄存器(Quad-word, Q0-Q15)。每个 Q0-Q15 寄存器映射到一对D寄存器,如下图所示。
在这里插入图片描述相比之下,AArch64 具有 31 31 31 64 64 64 位通用 Arm 寄存器和 1 1 1 个具有不同名称的特殊寄存器,这取决于使用它的上下文。这些寄存器可以被看作是 31 31 31 64 64 64 位寄存器(X0-X30)或 31 31 31 32 32 32 位寄存器(W0-W30)。

AArch64 具有 32 32 32 x 128 128 128 位 NEON 寄存器(V0-V31,Vector Registers)。这些寄存器也可以看作是 32 32 32Sn寄存器(Single-word Registers) 64 64 64Dn寄存器(Double-word Registers)

在这里插入图片描述(也就是说,V 寄存器有 128 位,D 寄存器 64 位,S寄存器 32 位,可以将 V 寄存器拆开使用)

3. NEON 命名方式

  • 变量命名方式:

    • baseWxLxN_t
      • base:是基础数据类型
      • W:是基础类型的宽度
      • L:是向量的长度
      • N:是向量数组的个数
    • uint8x16_tuint8x16x3_t
  • 函数命名方式:

    • ret v[p][q][r]name[u][n][q](args)
      • ret:返回值类型
      • v:表示vector
      • q:饱和运算,溢位后,为自动限制在数据类型的最大范围内
      • r:圆整操作
      • name:SIMD指令名称
      • u:unsigned
      • n:narrow
      • q:做后缀表示128位满位宽寄存器运算 quarter*32

4. 优化技巧

  • 热点函数涉及到大量 IO 读写操作时,数据的内存地址尽量与 NEON 数组或系统位数对齐,如32位对齐,可降低访问开销;

  • 重点优先搞 NEON 指令并行计算,能大幅降低开销;

  • 大部分的 NEON 问题会出在存取、移动指令的滥用、混乱使用上(neon的寄存器和普通arm的寄存器是分开,也就是说arm的普通指令和neon指令之间不可以有过多的数据交换,但是sse没有这个限制?待验证);

  • for 循环:

    • for (b = 0; b < num; b++)
    • 可改成 for (b = 0; b < num - 3; b += 4)
    • 或者 for (b = num - 1; b >= 3; b -= 4)
    • 需注意结尾不能整除的几个还是用非SIMD方式计算:
      • 原始:for (i = 0; i < size; i++);
      • 并行:for (i = 0; i < size - 3; i += 4);
      • 扫尾:for (; i < size; i ++)。
  • 数组索引取值:

    • 数组索引以及索引内部涉及运算的,尽量换成指针偏移加减来做;
    • 避免大范围索引跳跃,减少 cache miss
  • 内存使用:

    • 优先用局部变量,而非 malloc 堆内存,减少 cache miss;
    • 针对具体变量类型,手动 for 循环并行拷贝值,可能比 memcpy() 函数更高效,因为 memcpy 内部还涉及大量判断,以保证平台兼容性;
    • 用NEON指令时,4 路运算的数组(128位=16字节),内存地址最好要 16 字节对齐。
  • 指令运算

    • 矩阵乘场景,在不大幅增加寄存器变量的前提下,外部的A也最好并行多读几路数据进来,跟B的各列运算,减少B各列的读取次数;
    • 乘加指令,add 和 mul 可以合并为 mla,一条指令完成乘加操作。
  • C 语言编码级考虑

    • C 语言中一条事件的处理函数尽可能在一个源文件中(便于编译器自动向量化);
    • switchif else 快,而且代码整洁。
  • 深入理解计算机系统

    • 组织代码结构,善用 CPU 缓存,数据段/代码段连续可以提高 CPU 缓存命中率;
    • 极简函数时,尽量 inline 展开,减少函数调用栈的开销;
    • 消除不必要的存储器引用,如 for 循环中 *dest = *dest - nums[i],可用中间变量替换 *dest,for 循环后再赋值给 *dest,可减少 for 内的一次读写操作;
    • 简单的循环展开,编译器可以自己完成,优化选项 O2 及以上,或者命令 -funroll-loops(O2 及以上自带),可调用 gcc 进行循环展开;
    • 能用整型不用浮点,整数乘法/加法和浮点加法,只用一个周期,浮点乘法需要2个周期。
  • NEON 与 SSE 在寄存器处理数据时有一些区别。在 NEON 中,通常需要先将要处理的数据加载到 NEON 寄存器,然后执行 SIMD 操作。这与 SSE 有一些不同,因为 SSE 寄存器(XMM 寄存器)可以直接与内存交互(这一步可能格外耗时,需要特别注意)。在 NEON 中,加载数据到 NEON 寄存器通常包括以下步骤:

    1. 从内存加载数据到通用寄存器(通常是ARM通用寄存器)。
    2. 将数据从通用寄存器传送到 NEON 寄存器。

    然后,您可以在 NEON 寄存器上执行 SIMD 操作,例如矢量加法、矢量乘法等。

    这与 SSE 不同,因为 SSE 寄存器可以直接从内存加载数据,而不需要中间步骤。这可以在 SSE 指令中实现,而不需要将数据先加载到通用寄存器中。

    总之,NEON 需要额外的步骤来加载数据到寄存器,然后才能进行 SIMD 操作,而 SSE 可以更直接地在寄存器中操作数据。这是因为不同架构和指令集设计的差异。

5. 优化 NEON 代码(Armv7-A内容,但区别不大)

5.1 优化 NEON 汇编代码

考虑处理器如何集成 NEON 技术的实现定义方面,因为针对特定处理器优化的指令序列可能在不同的处理器上具有不同的时序特征,即使 NEON 指令周期时序相同。

为了从手写的 NEON 代码中获得最佳性能,有必要了解一些底层硬件特性。特别是,程序员应该意识到流水线和调度问题、内存访问行为和调度危害。

5.1.1 Cortex-A 处理器之间的 NEON 管道差异

Cortex-A8 和 Cortex-A9 处理器共享相同的基本 NEON 管道,尽管在如何将其集成到处理器管道中存在一些差异。Cortex-A5 处理器包含一个完全兼容的简化 NEON 执行管道,但它是为尽可能最小和最低功耗的实现而设计的。

5.1.2 内存访问优化


TLB(Translation Lookaside Buffer) 是计算机系统中的一种硬件缓存,用于加速虚拟地址到物理地址的转换过程。TLB 的每个条目称为 TLB entry(TLB条目)它存储了一组虚拟地址和相应的物理地址之间的映射关系。TLB 通常位于 CPU 内部,用于提高内存访问的速度。

当程序执行时,CPU 需要将虚拟地址(由程序使用)转换为物理地址(在内存中实际存储数据的地址)。这个地址转换通常由操作系统的内存管理单元(MMU)来执行。MMU 将虚拟地址映射到物理地址,并在需要时将这些映射关系存储在 TLB 中,以便以后的访问可以更快地完成,而无需再次执行昂贵的地址转换操作。

TLB entry 通常包括以下信息:

  1. 虚拟地址(Virtual Address):程序使用的地址。
  2. 物理地址(Physical Address):在内存中实际存储数据的地址。
  3. 标志位(Flags):包括权限信息(例如,读、写、执行权限)和其他控制信息。

当 CPU 需要访问内存中的数据时,它首先查看 TLB 来查找虚拟地址和物理地址之间的映射关系。如果找到了匹配的 TLB entry,那么物理地址将用于访问内存,这将显著提高内存访问速度。如果没有找到匹配的 TLB entry,CPU 将向 MMU 请求执行地址转换,并将结果存储在 TLB 中以供将来使用。

总之,TLB entry 是 TLB 中存储的虚拟地址到物理地址映射的单元,用于加速计算机内存访问的过程。这有助于提高系统的性能和效率。


L1 和 L2 通常是指计算机的缓存层次结构中的两个不同级别的缓存:

  • L1 Cache(一级缓存)
    L1 缓存是最接近 CPU 核心的缓存级别,通常位于 CPU 内部或非常靠近 CPU 核心。它是一个小而快速的缓存,用于存储最常用的数据和指令,以提高 CPU 的性能。
    L1 缓存分为两个部分,分别是指令缓存(Instruction Cache)数据缓存(Data Cache)。指令缓存存储 CPU 指令,而数据缓存存储处理数据。
    由于其靠近 CPU 核心的位置,L1 缓存通常具有非常低的访问延迟。
  • L2 Cache(二级缓存)
    L2 缓存位于 L1 缓存之上,通常在 CPU 内部或者与 CPU 核心相对较近,但比 L1 缓存大。
    它也用于存储数据和指令,但比 L1 缓存更大,能够容纳更多的数据。
    L2 缓存的访问延迟通常比 L1 缓存稍高,但仍然比主内存的访问延迟要低。
    有些计算机架构具有多个 L2 缓存层,通常是 L2 和 L3,L3 通常比 L2 更大,但访问延迟更高。

这两个缓存级别的存在是为了提高计算机的性能。L1 缓存专注于存储最常用的数据和指令,因此它们可以更快地被 CPU 核心访问。如果数据不在 L1 缓存中,CPU 将在 L2 缓存中查找,如果还不在,那么将从主内存中获取数据。通过在多个缓存层之间进行数据访问,计算机可以更有效地管理内存访问,从而提高整体性能。


NEON 单元很可能会处理大量数据,例如数字图像。一个重要的优化是确保算法以最适合缓存的方式访问数据。这样可以从 L1 和 L2 缓存中获得最大的命中率(hit rate)。考虑活动内存位置的数量也很重要。在 Linux 下,每个 4KB 的页面都需要一个单独的 TLB 条目。Cortex-A9 处理器有多个 32 32 32 个元素的 micro-TLB 和一个 128 128 128 个元素的主 TLB,之后它将开始使用 L1 缓存来加载页表条目(page table entry)。一种典型的优化是安排算法以适当的大小处理图像数据,以最大限度地提高缓存和 TLB 命中率。

支持 交错(interleaving)反交错(de-interleaving) 的指令可以为性能改进提供很大的空间。VLD1 从内存加载寄存器,没有去交错。然而,其他 VLDn 操作使我们能够加载、存储和反交错包含两个、三个或四个相同大小的 8 8 8 16 16 16 32 32 32 位元素的结构。VLD2 加载两个或四个寄存器,去交错的偶数和奇数元素。例如,这可以用于分割左通道和右通道立体声音频数据,如下图所示。类似地,VLD3 可用于将 RGB 像素分割为单独的通道,相应地,VLD4 可用于 ARGB 或 CMYK 图像。

在这里插入图片描述

载入去交错的示例

上图显示了用 VLD2.16( 16 16 16 字节) 从 R1 指向的内存中加载两个 NEON 寄存器。这在第一个寄存器中产生 4 4 4 16 16 16 位元素,在第二个寄存器中产生 4 4 4 16 16 16 位元素,相邻的成对左值和右值被分隔到每个寄存器中。

相关文章:

NEON优化:性能优化经验总结

NEON优化&#xff1a;性能优化经验总结 1. 什么是 NEONArm Adv SIMD 历史 2. 寄存器3. NEON 命名方式4. 优化技巧5. 优化 NEON 代码(Armv7-A内容&#xff0c;但区别不大)5.1 优化 NEON 汇编代码5.1.1 Cortex-A 处理器之间的 NEON 管道差异5.1.2 内存访问优化 Reference: NEON优…...

C++ 并发编程实战 第九章

目录 9.1 线程池 9.1.1 最简易可行的线程池 9.1.2 等待提交给线程池的任务完成运行 9.1.3等待其他任务完成的任务 9.1.4 避免任务队列上的争夺 9.1.5 任务窃取 9.2 中断线程 9.2.1 发起一个线程&#xff0c;以及把他中断 9.2.2 检测线程是否被中断 9.2.3 中断条件变…...

【Java】super 关键字用法

目录 this与super区别 1.访问成员变量-示例代码 继承中构造方法的访问特点 2.访问构造方法-示例代码&#xff1a; 继承中成员方法访问特点 3.访问成员方法-示例代码&#xff1a; super 关键字的用法和 this 关键字相似 this : 代表本类对象的引用super : 代表父类存储空间…...

前端笔试题总结,带答案和解析

1. 执行以下程序&#xff0c;输出结果为&#xff08;&#xff09; var x 10; var y 20; var z x < y ? x:y; console.log(xx;yy;zz);A x11;y21;z11 B x11;y20;z10 C x11;y21;z10 D x11;y20;z11 初始化x的值为10&#xff0c;y的值为20&#xff0c;x < y返回结果为tru…...

Omniverse Machinima

Omniverse Machinima App | NVIDIA Omniverse Machinima 是 NVIDIA 推出的一款实时动画创作工具&#xff0c;可用于在虚拟世界中制作和操纵角色及其环境。该工具使用 Universal Scene Description (USD) 作为其通用场景描述格式&#xff0c;可与多种 3D 建模、动画和渲染应用程…...

【测试人生】游戏业务测试落地精准测试专项的一些思路

精准测试在互联网领域有广泛的应用。以变更为出发点&#xff0c;通过对变更内容进行分析&#xff0c;可以确定单次变更具体涉及到哪些模块和功能点&#xff0c;以及是否存在夹带风险&#xff0c;从而从QA的视角&#xff0c;可以知道哪些功能模块需要做测试&#xff0c;以及哪些…...

Redis 数据类型底层原理

String 内部编码有三种&#xff1a;int、embstr、raw int&#xff1a;如果一个字符串对象保存的是整数值&#xff0c;并且这个整数值可以用 long类型来表示(不超过 long 的表示范围&#xff0c;如果超过了 long 的表示范围&#xff0c;那么按照存储字符串的编码来存储&#xf…...

EasyEdge 智能边缘控制台通过sdk发布应用

离线部署SDK生成 模型部署完成后会出现下载SDK的按钮&#xff0c;点击按钮下载SDK并保存好SDK。 进入EasyDL官网的技术文档 安装智能边缘控制台 跟着教程&#xff0c;完成安装&#xff1a;点此链接 树莓派4b是Linux arm64的架构&#xff0c;点击对应的链接进行下载。 下载完成…...

centos软件设置开机启动的方式

以下以redis作为案例&#xff1a; 开机启动方式一 [Unit] Descriptionredis-server Afternetwork.target [Service] Typeforking # 这里需要修改自己的配置文件 ExecStart/usr/local/bin/redis-server /etc/redis/redis.conf PrivateTmptrue [Install] WantedBymulti-user.tar…...

二叉树和堆

二叉树不存在度大于2的结点&#xff08;每个根最多只有两个子结点&#xff09;二叉树的子树有左右之分&#xff0c;次序不能颠倒&#xff0c;因此二叉树是有序树 两个特殊的二叉树——&#xff08;满二叉树&#xff0c;完全二叉树&#xff09; 满二叉树——每个根结点都有左右…...

洛谷P5732 【深基5.习7】杨辉三角题解

目录 题目【深基5.习7】杨辉三角题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1传送门 代码解释亲测 题目 【深基5.习7】杨辉三角 题目描述 给出 n ( n ≤ 20 ) n(n\le20) n(n≤20)&#xff0c;输出杨辉三角的前 n n n 行。 如果你不知道什么是杨辉三角&#xf…...

Docker 精简安装 Nacos 2.2.1 单机版本

准备工作&#xff1a; 1&#xff09;已安装docker 2&#xff09;数据库准备&#xff0c;演示使用MySql5.7版本 1、拉取 [rootTseng-HW ~]# docker pull nacos/nacos-server:v2.2.1 v2.2.1: Pulling from nacos/nacos-server 2d473b07cdd5: Already exists 77c5a601c050: Pul…...

IntelliJ IDEA配置Cplex12.6.3详细步骤

Cplex12.6.3版IntelliJ IDEA配置详细步骤 一、Cplex12.6.3版下载地址二、Cplex安装步骤三、IDEA配置CPLEX3.1 添加CPLEX安装目录的cplex.jar包到项目文件中3.2 将CPLEX的x64_win64文件夹添加到IDEA的VM options中 四、检查IDEA中Cplex是否安装成功卸载Cplex 一、Cplex12.6.3版下…...

2023 年最佳多 GPU 深度学习系统指南

动动发财的小手&#xff0c;点个赞吧&#xff01; 本文[1]提供了有关如何构建用于深度学习的多 GPU 系统的指南&#xff0c;并希望为您节省一些研究时间和实验时间。 1. GPU 让我们从有趣&#xff08;且昂贵&#xff09;的部分开始&#xff01; 购买 GPU 时的主要考虑因素是&am…...

Kotlin异常处理runCatching,getOrNull,onFailure,onSuccess(1)

Kotlin异常处理runCatching&#xff0c;getOrNull&#xff0c;onFailure&#xff0c;onSuccess&#xff08;1&#xff09; fun main(args: Array<String>) {var s1 runCatching {1 / 1}.getOrNull()println(s1) //s11&#xff0c;打印1println("-")var s2 ru…...

【深入探究人工智能】:历史、应用、技术与未来

深入探究人工智能 前言人工智能的历史人工智能的应用人工智能的技术人工智能的未来当代的人工智能产物结语&#x1f340;小结&#x1f340; &#x1f389;博客主页&#xff1a;小智_x0___0x_ &#x1f389;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &am…...

【设计模式】五、原型模式

文章目录 概述示例传统的方式的优缺点原型模式原理结构图-uml 类图 原型模式解决克隆羊问题的应用实例Sheep类实现clone()运行原型模式在 Spring 框架中源码分析 深入讨论-浅拷贝和深拷贝浅拷贝的介绍 小结 概述 示例 克隆羊问题 现在有一只羊 tom&#xff0c;姓名为: tom, 年…...

day36-注解

1. 注解 1.1 注释和注解的区别&#xff1f;&#xff08;掌握&#xff09; 共同点&#xff1a;都可以对程序进行解释说明。 不同点&#xff1a;注释&#xff0c;是给程序员看的。只在Java中有效。在class文件中不存在注释的。 ​ 当编译之后&#xff0c;会进行注释擦除。 ​…...

【C语言数据结构——————栈和队列4000字详解】

欢迎阅读新一期的c语言数据结构模块————栈和队列 ✒️个人主页&#xff1a;-_Joker_- &#x1f3f7;️专栏&#xff1a;C语言 &#x1f4dc;代码仓库&#xff1a;c_code &#x1f339;&#x1f339;欢迎大佬们的阅读和三连关注&#xff0c;顺着评论回访&#x1f339;&#…...

电子地图 | VINS-FUSION | 小觅相机D系列

目录 一、相关介绍 二、VINS-FUSION环境安装及使用 &#xff08;一&#xff09;Ubuntu18.04安装配置 1、Ubuntu下载安装 2、设置虚拟内存&#xff08;可选&#xff09; &#xff08;二&#xff09;VINS-FUSION环境配置 1、ros安装 2、ceres-solver安装 3、vins-fusion…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

Pinocchio 库详解及其在足式机器人上的应用

Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库&#xff0c;专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性&#xff0c;并提供了一个通用的框架&…...

Android第十三次面试总结(四大 组件基础)

Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成&#xff0c;用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机&#xff1a; ​onCreate()​​ ​调用时机​&#xff1a;Activity 首次创建时调用。​…...

【MATLAB代码】基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),附源代码|订阅专栏后可直接查看

文章所述的代码实现了基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),针对传感器观测数据中存在的脉冲型异常噪声问题,通过非线性加权机制提升滤波器的抗干扰能力。代码通过对比传统KF与MCC-KF在含异常值场景下的表现,验证了后者在状态估计鲁棒性方面的显著优…...

毫米波雷达基础理论(3D+4D)

3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文&#xff1a; 一文入门汽车毫米波雷达基本原理 &#xff1a;https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...

Leetcode33( 搜索旋转排序数组)

题目表述 整数数组 nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c;nums 在预先未知的某个下标 k&#xff08;0 < k < nums.length&#xff09;上进行了 旋转&#xff0c;使数组变为 [nums[k], nums[k1], …, nums[n-1], nums[0], nu…...

comfyui 工作流中 图生视频 如何增加视频的长度到5秒

comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗&#xff1f; 在ComfyUI中实现图生视频并延长到5秒&#xff0c;需要结合多个扩展和技巧。以下是完整解决方案&#xff1a; 核心工作流配置&#xff08;24fps下5秒120帧&#xff09; #mermaid-svg-yP…...

Sklearn 机器学习 缺失值处理 获取填充失值的统计值

💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 使用 Scikit-learn 处理缺失值并提取填充统计信息的完整指南 在机器学习项目中,数据清…...

Axure 下拉框联动

实现选省、选完省之后选对应省份下的市区...