浅谈单片机的gcc优化级别__以双音频信号发生器为例

IDE:  CLion

HOST: Windows 11

MinGW:x86_64-14.2.0-release-posix-seh-ucrt-rt_v12-rev0

GCC: arm-gnu-toolchain-13.3.rel1-mingw-w64-i686-arm-none-eabi

一、简介

        gcc有多种优化级别,一般不选择的情况下,IDE默认是按照-Og或这-O2优化的。

        以gcc编译器为例,浅谈一下优化级别,我们常见的优化一般是指gcc的-O2、-Og。除此之外,gcc还有-Os等一系列优化,链接器也有优化级别。

        基于单片机的开发,如果关注的是生成代码的大小,那么可以考虑-Os和-Oz。如果在乎性能的话,可以尝试-O2以上的优化级别

-O优化

一般有下面几个级别。当然上面还有-O4甚至-O5,不过个人感觉和-O3差不多.

  • -O0:无优化,适合调试。
  • -O1:基本优化,适合快速迭代开发。
  • -O2:中级优化,适合大多数生产环境。
  • -O3:高级优化,适合对性能要求较高的应用。
  • -Ofast:极端优化,适合对性能要求极高且对标准合规性要求不高的应用。
  • -Og:调试优化,适合开发和调试阶段。
  • -Os:优化生成的代码大小,而不是性能。
  • -Oz:极度优化大小,进一步优化生成的代码大小,比 -Os更激进,可能会牺牲一些性能。
  • -flto:在链接阶段进行优化,允许进行跨文件的优化,进一步提高性能,但耗时间
  • -fprofile-generate(生成配置文件) 和 -fprofile-use(基于配置文件的优化)

从下面生成的代码体积来看优化对程序的影响

【-O0】

【-O1】

【-O2】

【-O3】

【-Ofast】

【-Og】

【-Os】

【-Oz】

-flto

        启用链接时优化,它会跨文件进行优化,会把所有东西混杂起来再优化,同时也会影响调试信息的生成。这就意味着,前面的-O级优化还能把编译出的二进制文件与你的源码对应起来,现在只能与反汇编对应。

        如下图,你甚至能看到CPU的那12个寄存器,此时展现在调试窗口的就是反汇编了。如果你想要让IDE调试程序时与源码对应,那么就需要加上-g编译标志

做个对比,虽然仅从代码量来观察是片面了许多,但多少能反映一些(原先代码忘记做速度测试了)。

上图是未开-flto,下图是开了-flto

【-O1】

【-O2】

【-O3】

【-Ofast】

【-Os】

【-Oz】

只不过有时候同样一份代码,用不同的方式优化可能还会报错,比如下面是-Og -flto优化,因为链接库的某种不知名原因

上述的-O级优化其实是由一系列单项优化组成的,可以组合,更适合竞赛宝宝体质的#pragram

-fwhole-program
目标:在整个程序范围内进行优化。
特点:
允许编译器在链接阶段对整个程序进行全局优化。
通常与 -O3 一起使用,以获得更高的性能。
-fprofile-generate 和 -fprofile-use
目标:基于运行时数据进行优化。
特点:
-fprofile-generate:生成配置文件。
-fprofile-use:使用生成的配置文件进行优化。
可以显著提高性能,特别是在热点路径上。
-fipa-cp-algorithm
目标:改进跨过程常量传播算法。
特点:
用于改进跨过程的常量传播,提高代码性能。
-fipa-pta
目标:改进指针分析。
特点:
用于改进指针分析,提高代码性能。 
-funroll-loops
目标:展开循环。
特点:通过展开循环减少循环开销,提高性能。
-finline-functions
目标:内联函数。
特点:自动内联小函数,减少函数调用开销。
-fomit-frame-pointer
目标:省略帧指针。
特点:在函数调用中省略帧指针,减少寄存器使用,提高性能。
-fstrict-aliasing
目标:启用严格的别名规则。
特点:允许编译器进行更激进的优化,假设不同类型的指针不会指向同一内存地址。
-ftree-vectorize
目标:启用向量化优化。
特点:将循环中的标量操作转换为向量操作,利用 SIMD 指令集提高性能。
-floop-interchange
目标:交换循环顺序。
特点:优化嵌套循环的顺序,提高缓存利用率。
-floop-strip-mine
目标:分割循环。
特点:将大循环分割成多个小循环,提高缓存利用率。
-floop-block
目标:块划分循环。
特点:将循环体划分为多个块,提高缓存利用率。
-fgraphite
目标:启用 Graphite 循环变换框架。
特点:使用高级循环变换技术优化循环性能。
-fipa-sra
目标:启用跨过程结构体拆解。
特点:在跨过程调用中拆解结构体,减少内存访问开销。

链接器也有一系列优化,就是不常用,包括上面提到的一系列组合,对于单片机开发来说

-Wl,--hash-style=both
目标:使用两种哈希风格。
特点:链接器使用两种哈希风格(SYSV 和 GNU),提高符号查找效率。
-Wl,--no-undefined
目标:禁止未定义的符号。
特点:链接器在链接时检查未定义的符号,确保所有符号都已定义。
-Wl,--no-merge-exidx-entries
目标:禁止合并异常索引条目。
特点:防止链接器合并异常索引条目,确保异常处理的准确性。
-Wl,--sort-common
目标:按大小排序公共符号。
特点:链接器按大小排序公共符号,提高内存布局的效率。
-Wl,--sort-section=name
目标:按名称排序节区。
特点:链接器按名称排序节区,提高内存布局的效率。
-Wl,--no-keep-memory
目标:释放内存。
特点:链接器在链接过程中释放不再需要的内存,减少内存占用。

 二、测试

示例工程

        这里做了一点点简单不那么严谨的小测试,使用的测试工程为下面链接中的双音频信号发生器ichliebedich-DaCapo/STM32F407VET6: stm32f407vet6 (github.com)

-O0:

       结果很感人,烧录时一切正常

        但按下按键后还没怎么执行就卡住了。

        在卡住之后,我们停下来可以清楚地看到堆栈爆了(栈区溢出,下方蓝色的msp寄存区),直接的影响就是LVGL处理事件时,访问数组直接越界。换句话说,如果下次碰到了LVGL数组越界,那么就要怀疑是栈区溢出了。

现在看一看编译大小

        -gc-sections是去除不用的段,--print-memory-usage是打印内存分布,Map=${BIN_DIR}/${PROJECT_NAME}.map是生成map映射文件。当然,前面都得有-Wl

add_link_options(-Wl,-gc-sections,--print-memory-usage,-Map=${BIN_DIR}/${PROJECT_NAME}.map)

-O1:

        同样的代码,使用-O1可以很明显地看到优化情况

        下面将以以内置的FPS组件显示,在128个数据点、线性插值算法、800Hz(只是虚拟的,不是真的)下进行测试

        FPS组件代码是基于LVGL的文本框组件写的一个小类,没有做什么性能上的优化,但简单测试衡量一下性能变化还是可以做到的。


/*** @brief 工具类*/
class Tools
{
public:static inline auto fps_init(Font font, Coord x = 0, Coord y = 40, Coord width = 60, Coord height = 20) -> void;// 显示fpsstatic inline auto fps(bool time = true) -> void;static inline auto restart_fps() -> void;static inline auto set_right() -> void;static inline auto set_left() -> void;static inline auto set_center() -> void;static inline auto clear_fps() -> void;private:// 获取时间static inline auto get_tick() -> uint32_t;// 单线程更新事件static inline auto update_tick() -> void;private:static inline Obj_t label_fps{};static inline uint32_t count = 0;static inline uint32_t tick = 0;
};/*** @brief fps功能初始化* @param font 指定字库中要有fps和十位数字,字体大小为13即可* @param x x轴* @param y y轴* @note 默认文本框为60*20,即宽60,高20,且文本为左对齐。*/
auto Tools::fps_init(Font font, Coord x, Coord y, Coord width, Coord height) -> void
{Text label;label.init_font(font);
#if SIMPLE_FPSlabel.init(label_fps, x, y, width, height, "");
#elselabel.init(label_fps, x, y, 60, 80, "fps\n0");
#endif}/*** @brief fps显示* @note 启用该功能之前必须先调用fps_init进行必要的初始化。启用fps显示,即在需要的地方调用本函数*      默认显示一帧需要的时间单位为ms*/
auto Tools::fps(bool time) -> void
{
#if SIMPLE_FPSchar buf[7];if (time){// 显示一帧的时间sprintf(buf, "%.2fms", 1.0 * get_tick() / (count++));} else{// 显示帧率sprintf(buf, "%.2f", 1000.0 * (count++) / get_tick());}Text::set_text(buf, label_fps);
#elsechar buf[9];// 显示一帧的时间sprintf(buf, "fps\n%.2f", 1.0*get_tick() / (count++));// 显示帧率
//    sprintf(buf, "fps\n%.2f", 1000.0*(count++))/get_tick();Text::set_text(buf, label_fps);
#endif
}auto Tools::get_tick() -> uint32_t
{uint32_t temp_tick = lv_tick_get();// 防止溢出if (temp_tick < tick)temp_tick += (0xFFFF'FFFF - tick);elsetemp_tick -= tick;return temp_tick;
}auto Tools::update_tick() -> void
{tick = lv_tick_get();
}/*** @brief 重启fps*/
auto Tools::restart_fps() -> void
{update_tick();count = 0;
}auto Tools::set_right() -> void
{Text::set_text_align(LV_TEXT_ALIGN_RIGHT, label_fps);
}auto Tools::set_center() -> void
{Text::set_text_align(LV_TEXT_ALIGN_CENTER, label_fps);
}auto Tools::set_left() -> void
{Text::set_text_align(LV_TEXT_ALIGN_LEFT, label_fps);
}auto Tools::clear_fps() -> void
{Text::set_text("", label_fps);}

可以看出,一帧所用时间为36.88ms左右,并且屏幕右侧有严重的漏墨现象,这是由于绘制像素点函数LCD_Set_Pixel不够卖力(主频不够高)导致的

-O2:

        接下来使用-O2级别,同上面相比,我们可以看到RAM和Flash都略微增加了少许

 

        接下来测试一下性能,从右上角的36.79ms可以看出,相比-O1可能有了那么一点点提升(因为不能排除误差),从观察效果来看,漏墨现象也是挺严重的。在我印象中,应该比-O1强一些才对,可能是这次没发挥好(不同工程、相同的优化级别,显现的效果是不同的)

 -Og:

        接下来我们看看平时最常用的调试级别优化能拿出怎样的成绩吧。首先是代码体积比-O1还小了一点,内存占用相同。

        接下来看看性能,37.16ms,可能是由于调试信息的原因性能就略逊一筹,不过与-O1、-O2也大差不差

-O3:

        接下来有请-O3大佬, 一出手就是非同凡响,RAM占用些许提升,ROM大幅提升

从性能上来看,竟然与前面差不多,那么可以说明一个问题,现在性能的瓶颈不在于算法,而在于打点速度。真是失策,测了这么多有种白费的感觉。

————编译优化———— 

        不过接下来换种算法测试一下,就以样条算法为例,这个与贝塞尔曲线差不多的速度,比线性插值慢,但稳定多了,帧率最后会趋于一个稳定的值,所以测试结果相对要可靠一些。

        由于工程不变,所以就不继续展示代码大小了

 -O1

-O2

-Og

-O3

-Ofast

代码体积比-O3多了一点点,-O3 -ffast-math、-O4、-O5在代码体积上与-O3完全一致

该优化被clang淘汰掉了,取而代之的是-O3 -ffast-math。gcc还有是-Ofast的

-Os

代码确实小了一些

看看性能,63.63ms与-Og差不多

-Oz

看来代码的体积已经被压缩到极致了

性能与-Os差不多,但与-Ofast比起来就相对明显

         至于-flto优化这个我现在无法测试,因为改了文件组织编译方式,把大部分文件都分别编译为静态库,然后再统一链接成elf文件。所以无法使用-flto,一使用就会出现找不到定义的错误。之前没改CMakelists前,使用-flto,代码体积上确有优化,但性能没有测试过。

    add_compile_options(-flto )

        -O3及以上的优化要慎重对待,上次基于样条算法编写一个模板函数,只有在-O2下可以正常运行,开-O3以上 会卡死,开-O2以下堆栈会爆,真是让人摸不到头脑。后来也不知改了什么,或许是改动了其他函数间接导致这个模板函数又行了,在-O1到-Ofast均可正常使用。

        编译器优化,很玄

         另提一嘴,在使用总大小相同的缓冲数组情况下,LVGL的双缓冲要优于单缓冲。设置双缓冲也很简单,只有在旁边另加一个静态数组,然后把数组名填入到lv_disp_draw_buf_init的第三个参数中

/*** @brief 初始化显示驱动* @tparam flush 涂色函数,有LCD驱动提供* @note 为了让lambda表达式可以不用捕获外部函数,只能使用函数模板。如果使用函数指针来传递就必须要显示捕获*/
template<void (*flush)(uint16_t, uint16_t, uint16_t, uint16_t, const uint16_t *)>
auto GUI::disp_drv_init() -> void
{// 在缓冲数组总大小同等的情况下,双缓冲明显优于单缓冲static lv_disp_draw_buf_t draw_buf_dsc;static lv_color_t buf_2_1[MY_DISP_HOR_RES * MY_DISP_BUF_SIZE];static lv_color_t buf_2_2[MY_DISP_HOR_RES * MY_DISP_BUF_SIZE];lv_disp_draw_buf_init(&draw_buf_dsc, buf_2_1, buf_2_2,MY_DISP_HOR_RES * MY_DISP_BUF_SIZE);   /*Initialize the display buffer*/lv_disp_drv_init(&disp_drv);                    /*Basic initialization*/disp_drv.hor_res = MY_DISP_HOR_RES;disp_drv.ver_res = MY_DISP_VER_RES;
// C环境下就不要使用lambda表达式,自行定义flush函数disp_drv.flush_cb = [](lv_disp_drv_t *, const lv_area_t *area, lv_color_t *color_p){flush(area->x1, area->y1, area->x2, area->y2, (const uint16_t *) color_p);};disp_drv.draw_buf = &draw_buf_dsc;lv_disp_drv_register(&disp_drv);
}

————链接优化———— 

补充:

        改了一下CMakelists组织目录,接下来可以使用-flto优化,不过可能是由于资源限制或者哪里忘了配置,我使用这个优化选项,只能串行编译 9 个 LTRANS 任务,无法使用并行,所以链接时会比较慢。以此工程为例,编译用了6.32s左右,链接用了7.68s左右。

        下面将重复上面条件,在编译优化的基础上继续测试链接优化

-O0

        在不开编译优化的情况下,链接优化会使代码膨胀

         作为对比,下面是没有启用链接优化前

 -O1

        上图是链接优化后,下图是链接优化前,可以看到RAM略微上升,ROM小幅下降

性能为63.20ms,相较与编译优化的63.32ms,还是能看到些许优化效果的

-O2

        上图为链接优化,相比与下图,内存占用略微减少,ROM小幅提升

 

一帧所用为62.98ms,相较于先前的63.04ms,差不多

 -Og

内存分布上来看,RAM和FLASH都下降了

相较于先前的63.63ms,略微提升

-O3

相较于先前,ROM涨幅比较大,内存略微下降

相较于先前的63.03ms,此时的62.95ms略微提升

 -Ofast

        ROM小幅增加,RAM略微下降

        可以看到性能上与前面的-O3也差不多

 -Os

此时可以看到RAM略微下降,ROM小幅下降

        那么看看性能如何呢?结果很amazing啊,性能虽然连-O1都比不了,与没开链接优化前的63.63ms也差不多,但屏幕干净了许多,不怎么漏墨

 -Oz

虽比先前的占用略微减小,但与链接优化的-Os差不多

与上面的-Os差不多

三、结论 

        没有优化绘制算法前,不同优化级别差距还是比较明显的

        在经历了一系列手动优化后,我们可以看到除了-O0外,编译器(或连接器)优化对整体程序的性能优化并不怎么理想,对存储占用倒还算比较有用。换句话说,由于单片机上的计算资源一般并不怎么强,当软件手动优化到一定级别后,软件对整体性能的影响就不怎么大了,硬件的性能瓶颈对整体性能的制约占主要地位。

        事实上,以本次为例,开启优化仅能把性能从188ms提升到174ms。但如果优化算法、增加缓冲数组、开启硬件优化(使用DMA)等,却可以从174ms提升到前面的64ms左右。

        基于此,平时可以开-O3优化,不过要注意-O3下的优化很容易把一些变量优化掉,比如你随便定义了一个全局变量keyflag,然后在while循环里判断,那么很有可能会把它优化为寄存器,就导致即使接收到了按键信号,但寄存器也没有及时改变,进而无法正确及时处理按键任务。

        工程基本完善后,那么可以尝试-Oz,如果与-O3性能差不多,那么可以考虑使用-Oz,这样会进一步降低存储占用。工程确认无误后,可以开-flto进行最后的优化,进一步减少存储占用,并提高些许性能。

        

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

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

相关文章

Ceph MDS高可用架构探索:从零到一构建多主一备MDS服务

文章目录 Ceph实现MDS服务多主一备高可用架构当前 mds 服务器状态添加 MDS 服务器验证ceph集群当前状态当前的文件系统状态设置处于激活状态 mds 的数量MDS 高可用优化分发配置文件并重启 mds 服务 Ceph实现MDS服务多主一备高可用架构 Ceph 的元数据服务&#xff08;MDS&#…

PySpark 数据处理实战:从基础操作到案例分析

Spark 的介绍与搭建&#xff1a;从理论到实践_spark环境搭建-CSDN博客 Spark 的Standalone集群环境安装与测试-CSDN博客 PySpark 本地开发环境搭建与实践-CSDN博客 Spark 程序开发与提交&#xff1a;本地与集群模式全解析-CSDN博客 Spark on YARN&#xff1a;Spark集群模式…

使用GPT-SoVITS训练语音模型

1.项目演示 阅读单句话 1725352713141 读古诗 1725353700203 2.项目环境 开发环境&#xff1a;linux 机器配置如下&#xff1a;实际使用率百分之二十几&#xff0c; 3.开发步骤 1.首先是准备数据集&#xff0c;要求是wav格式&#xff0c;一到两个小时即可&#xff0c; 2.…

Python学习从0到1 day27 Python 高阶技巧 ③ 设计模式 — 单例模式

此去经年&#xff0c;再难同游 —— 24.11.11 一、什么是设计模式 设计模式是一种编程套路&#xff0c;可以极大的方便程序的开发最常见、最经典的设计模式&#xff0c;就是我们所学习的面向对象了。 除了面向对象外,在编程中也有很多既定的套路可以方便开发,我们称之为设计模…

3.2 软件需求:面对过程分析模型

面对过程分析模型 1. 需求分析的模型概述1.1 面对过程分析模型-结构化分析方法1.2 结构化分析的过程 2. 功能模型&#xff1a;数据流图初步2.1 加工2.2 外部实体&#xff08;数据源点/终点&#xff09;2.3 数据流2.4 数据存储2.5 注意事项 3. 功能模型&#xff1a;数据流图进阶…

Android Studio 运行模拟器无法打开avd

问题&#xff1a;已经下载了HAXM 打开模拟器时还是提示未下载HAXM&#xff0c;无法打开avd 解决方案&#xff1a; 控制面板 -> 启动或关闭Windows功能&#xff0c;打开图下两项&#xff0c;后重启电脑重启Android Studio&#xff1a;

Qt文件系统-二进制文件读写

实例功能概述 除了文本文件之外&#xff0c;其他需要按照一定的格式定义读写的文件都称为二进制文件。每种格式的二进制文件都有自己的格式定义&#xff0c;写入数据时按照一定的顺写入&#xff0c;读出时也按照相应的顺读出。例如地球物理中常用的SEG-Y格式文件&#xff0c;必…

ARXML汽车可扩展标记性语言规范讲解

ARXML: Automotive Extensible Markup Language &#xff08;汽车可扩展标记语言&#xff09; xmlns: Xml name space &#xff08;xml 命名空间&#xff09; xsd: Xml Schema Definition (xml 架构定义) 1、XML与HTML的区别&#xff0c;可扩展。 可扩展&#xff0c;主要是…

游戏引擎学习第六天

这节讲的内容比较多: 参考视频:https://www.bilibili.com/video/BV1apmpYVEQu/ XInput 是微软提供的一个 API&#xff0c;用于处理 Windows 平台上 Xbox 控制器&#xff08;包括有线和无线&#xff09;及其他游戏控制器的输入。它为开发者提供了一组函数&#xff0c;用于查询控…

vivado+modelsim: xxx is not a function name

xxx is not a function name vivado问题:xxx is not a function name原因 vivado问题:xxx is not a function name 在写verilog modelsim仿真时&#xff0c;遇到error&#xff1a;xxx is not a function name。 原因 该变量xxx在仿真文件里&#xff0c;如下图红框所示&#…

云计算在教育领域的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 云计算在教育领域的应用 云计算在教育领域的应用 云计算在教育领域的应用 引言 云计算概述 定义与原理 发展历程 云计算的关键技…

立体工业相机提升工业自动化中的立体深度感知

深度感知对仓库机器人应用至关重要&#xff0c;尤其是在自主导航、物品拾取与放置、库存管理等方面。 通过将深度感知与各种类型的3D数据&#xff08;如体积数据、点云、纹理等&#xff09;相结合&#xff0c;仓库机器人可以在错综复杂环境中实现自主导航&#xff0c;物品检测…

模拟鼠标真人移动轨迹算法-易语言

一.简介 鼠标轨迹算法是一种模拟人类鼠标操作的程序&#xff0c;它能够模拟出自然而真实的鼠标移动路径。 鼠标轨迹算法的底层实现采用C/C语言&#xff0c;原因在于C/C提供了高性能的执行能力和直接访问操作系统底层资源的能力。 鼠标轨迹算法具有以下优势&#xff1a; 模拟…

JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)

目录 基本使用内容 下载与安装 目录结构介绍 启动与关闭 启动 关闭 可能出现的问题及解决方案 问题一&#xff1a;启动时窗口一闪而过 问题二&#xff1a;端口号冲突 问题三&#xff1a;部署应用程序 总结 基本使用内容 Tomcat 服务器在 Java Web 开发中扮演着至关重…

PostgreSQL中如果有Left Join的时候索引怎么加

在PostgreSQL中&#xff0c;当你的查询包含多个LEFT JOIN和WHERE条件时&#xff0c;合理地添加索引可以显著提高查询性能。以下是一些具体的优化步骤和建议&#xff1a; 1. 分析查询 使用 EXPLAIN ANALYZE 命令分析你的查询&#xff0c;了解查询的执行计划&#xff0c;识别出连…

通过DNS服务器架构解释DNS请求过程

在前面的章节,这里,基于PCAP数据包和RFC文档详细介绍了DNS请求和响应的每个字段的含义。但是在现实的网络世界中,DNS请求和响应的数据包是怎么流动的,会经过哪些设备。本文将着重说明一下目前网络空间中DNS请求和响应的流动过程。 当前网络空间中比较常见DNS请求的流程如下…

aspose如何获取PPT放映页“切换”的“持续时间”值

aspose如何获取PPT放映页“切换”的“持续时间”值 项目场景问题描述问题1&#xff1a;从官方文档和资料查阅发现并没有对切换的持续时间进行处理的方法问题2&#xff1a;aspose的依赖包中&#xff0c;所有的关键对象都进行了混淆处理 解决方案1、找到ppt切换的持续时间对应的混…

GIT:如何查找已删除的文件的历史记录

首先你得知道文件的名称和路径 然后打开 gitlab&#xff0c;到项目中&#xff0c;仓库-> 文件 查找文件 复制文件名到可能存在过这个文件的分支当中&#xff0c;就能看到了

自动渗透测试与手动渗透测试

根据《渗透测试中发现的 5 种常见网络安全威胁》报告&#xff0c;渗透测试越来越受欢迎。预计到 2025 年&#xff0c;渗透测试市场规模将达到 45 亿美元。 什么是自动渗透测试&#xff1f; 自动化渗透测试工具可以快速有效地检查系统中是否存在已知的安全问题&#xff0c;即使…

使用elementUI实现表格行拖拽改变顺序,无需引入外部库

前言&#xff1a; 使用vue2element UI&#xff0c;且完全使用原生的拖拽事件,无需引入外部库。 如果表格数据量较大&#xff0c;或需要更多复杂功能&#xff0c;可以考虑使用 vuedraggable库&#xff0c;提供更多配置选项和拖拽功能。 思路&#xff1a; 1. 通过el-table的ro…