☰
asm-lessons 完全指南:从零入门 FFmpeg 手写汇编语言与 SIMD 向量优化
2026/10/2 1:55:18 网站建设 项目流程
  • 教程
  • 文档

【免费下载链接】asm-lessons

FFmpeg Assembly Language Lessons

项目地址:https://gitcode.com/GitHub_Trending/as/asm-lessons
点击查看免费下载

本指南以开源仓库asm-lessons(FFmpeg Assembly Language Lessons,FFmpeg 汇编语言学校)的 README.md 与三节课程文档为骨架,系统讲解这门为 FFmpeg 贡献者设计的汇编入门课程:它需要哪些前置知识、覆盖哪些核心主题(SIMD 向量编程、x86inc.asm 抽象层、分支循环、指令集演进、对齐与数据展开等),以及如何按顺序完成学习。读完后,你将理解 FFmpeg 手写汇编的编程范式,具备读懂并逐步编写可贡献给 FFmpeg 的 SIMD 汇编函数的基础能力。

课程概览:这是一门什么样的课

README.md 的开篇把仓库定位为 "FFmpeg School of Assembly Language"——一门面向编程爱好者的汇编语言学校。课程文档明确给出了它的目标与承诺:完成全部课程后,你将具备为 FFmpeg 贡献代码的能力。这并非夸张的宣传,因为课程本身正是围绕 FFmpeg 社区真实使用的汇编编程方式(x86inc.asm 宏体系、Intel 语法、手写 SIMD)展开的,课程中出现的函数签名、指令用法与 FFmpeg 实际代码保持一致。

仓库当前结构如下:

路径内容
README.md课程主页:欢迎语、前置知识、课程简介、翻译入口
lesson_01/index.md第 1 课:汇编与 SIMD 基础、寄存器、第一个 SIMD 函数
lesson_02/index.md第 2 课:分支与循环、常量、偏移、LEA 寻址
lesson_03/index.md第 3 课:指令集演进、指针偏移技巧、对齐、范围/符号扩展、洗牌
README.{zh,es,fr,tr}.md 及 lesson_0X/index.{zh,es,fr,tr}.md中、英、法、西、土五语言版本

README 还说明:仓库包含与每节课对应的课程与作业,其中作业(assignments)尚未上传;学习过程中遇到问题可在项目提供的 Discord 服务器中提问(入口见 README)。文档末尾提供了多语言翻译入口,包括 English、Français、Spanish、Turkish 与 中文,各课正文同样有多语言版本(如 第 1 课中文版、第 2 课中文版、第 3 课中文版)。

前置知识:学之前需要准备什么

README 明确列出了两条硬性要求,门槛并不高:

  • C 语言知识,尤其是指针。如果还不熟悉 C,建议先通读《The C Programming Language》这类经典教材。指针是本课程理解[srcq]解引用、[base + scale*index + disp]寻址等概念的基础。
  • 高中数学,具体指标量(scalar)与向量(vector)的区别、加法与乘法等基本运算。这为理解 SIMD "一条指令同时处理多个数据元素" 的思想做铺垫。

课程文档特别强调了一个事实:在 FFmpeg 里,甚至高中生都写过汇编代码。也就是说这门课对初学者是足够友好的——学习过程中大约一半是术语、一半是真正的知识。

课程核心内容精讲

三节课程是这门课的主体,下面按课程顺序梳理每一课的核心知识点与可验证的代码依据。

第 1 课:为什么 FFmpeg 坚持手写汇编(lesson_01/index.md)

汇编语言与 SIMD 的本质

第 1 课首先厘清概念:汇编语言是一种直接对应 CPU 指令的编程语言,人类可读的助记符(mnemonic)经汇编器(assembler)转换为 CPU 能执行的二进制机器码。FFmpeg 中的绝大部分汇编代码属于SIMD(Single Instruction Multiple Data,单指令多数据),即一条指令同时操作多个数据元素,这与传统"一次只处理一个数据元素"的标量编程形成对比。课程指出,SIMD 天然适合处理图像、视频、音频这类在内存中顺序排列的海量数据——这正是 FFmpeg 的核心业务场景。在 FFmpeg 语境中,"assembly function"、"SIMD"、"vectorise" 三个词可以互换使用,其他项目也可能称之为 "assembly kernels"。

为什么不用 intrinsics、不用编译器自动向量化

第 1 课给出了 FFmpeg 手写汇编的完整动机链:

  • 性能:手写汇编带来 10 倍甚至更高的加速非常常见(课程原文:"very common to get a 10x or more speed improvement"),这对实时无卡顿播放视频至关重要,同时还能省电、延长电池寿命。视频编解码是地球上被调用最频繁的函数之一,小优化乘以海量调用就是巨大的收益。
  • 对比 intrinsics:网上常有人用 intrinsics(映射到汇编指令的类 C 函数)来加速开发,但 FFmpeg 坚持手写。课程指出 intrinsics 通常比手写汇编慢 10%~15%(取决于编译器),且其 "匈牙利命名法" 风格可读性差;而 inline assembly(内联汇编)因可读性差、编译器支持不佳、难以维护,在 FFmpeg 中仅剩历史遗留的少数几处。
  • 对比编译器自动向量化:课程还回应了"编译器能帮你自动向量化"的说法,引用 dav1d 项目的测试:自动向量化约 2 倍加速,而手写版本可达 8 倍。

寄存器体系

课程介绍了 CPU 处理数据的核心载体——寄存器:CPU 不直接操作内存,而是先把数据加载进寄存器、处理后写回内存;汇编中一般不能直接把数据从一个内存位置复制到另一个内存位置而不经过寄存器。

  • 通用寄存器(GPR):可存放 64 位数据或内存地址(指针),支持加法、乘法、移位等运算。在 FFmpeg 汇编中 GPR 更多扮演 "脚手架" 角色,大部分复杂性被抽象掉。
  • 向量(SIMD)寄存器:可同时容纳多个数据元素,有四种:
寄存器位宽说明
mm(MMX)64 位历史遗留,已很少使用
xmm(XMM)128 位广泛可用
ymm(YMM)256 位使用时有一些注意事项
zmm(ZMM)512 位可用性受限

由于视频压缩/解压的计算大多是整数运算,课程以整数为主。一个 xmm 寄存器既可以看作 16 个字节(8 位)、8 个字(16 位)、4 个双字(32 位),也可以看作 2 个四字(64 位)。由此引出一套贯穿始终的数据尺寸术语(这些加粗字母在后续指令后缀中会反复出现):

  • byte —— 8 位数据
  • word —— 16 位数据
  • doubleword —— 32 位数据
  • quadword —— 64 位数据
  • doublequadword —— 128 位数据

x86inc.asm 抽象层

课程反复出现的%include "x86inc.asm"来自 x264、FFmpeg、dav1d 社区共同维护的轻量抽象层,作用是让汇编程序员的生活更轻松:它会把 GPR 标注为r0、r1、r2等逻辑名,你完全不用记忆具体寄存器名字;它提供的cglobal、RET、INIT_XMM等宏简化了函数定义与返回;它还能在你使用了某个指令集不支持的指令时给出提示。

第一个 SIMD 函数(lesson_01/index.md)

课程的第一个 SIMD 函数add_values值得逐行精读:

%include "x86inc.asm" SECTION .text ;static void add_values(uint8_t *src, const uint8_t *src2) INIT_XMM sse2 cglobal add_values, 2, 2, 2, src, src2 movu m0, [srcq] movu m1, [src2q] paddb m0, m1 movu [srcq], m0 RET

逐行拆解:

  • %include "x86inc.asm":引入上文提到的社区共享头文件。
  • SECTION .text:把代码放到可执行代码段,与存放常量数据的.data段相对。
  • 分号开头的行是注释(汇编中;相当于 C 的//),注释写出了该函数对应的 C 签名:static void add_values(uint8_t *src, const uint8_t *src2)。
  • INIT_XMM sse2:初始化函数使用 XMM 寄存器、采用 sse2 指令集(因为下文paddb是 sse2 指令)。
  • cglobal add_values, 2, 2, 2, src, src2(lesson_01/index.md):定义一个名为add_values的 C 函数。第一个参数 2 表示函数有两个参数;第二个 2 表示函数将使用两个 GPR(含参数);第三个 2 表示将使用两个 XMM 寄存器;最后src, src2是参数标签。旧代码可能不给参数命名,而是直接用r0、r1等寻址。
  • movu m0, [srcq]/movu m1, [src2q]:movu是movdqu(move double quad unaligned,非对齐 128 位移动)的缩写。方括号表示解引用[srcq]中的地址,等价于 C 中的*src;q后缀表示指针按 quadword 尺寸寻址(64 位系统上sizeof(*src) == 8,x86asm 在 32 位系统上会自动改用 32 位)。这两行是load(加载)操作。注意向量寄存器不写全名 xmm0,而用抽象名m0——这正是后续课程中"一套代码适配多种 SIMD 寄存器宽度"的关键。
  • paddb m0, m1(lesson_01/index.md):读作 "p-add-b"。p前缀表示 packed(打包/向量),用于区分向量指令与标量指令;b后缀表示按字节相加。它把两个寄存器中对应位置的 16 个字节两两相加(a+q、b+r、…、p+af)。
  • movu [srcq], m0:store(存储),把结果写回srcq指向的地址。
  • RET:函数返回的宏。几乎所有 FFmpeg 汇编函数都是就地修改参数数据而非返回新值,这也呼应了后续作业中"创建函数指针指向汇编函数并使用"的做法。

一个标量小例子

在进入 SIMD 之前,课程先用一段简单的标量代码演示指令语义(lesson_01/index.md):

mov r0q, 3 inc r0q dec r0q imul r0q, 5

第一行把立即数 3 存入寄存器 r0(作为 quadword)。Intel 语法中,源操作数在右、目标操作数在左,可读作r0q = 3(与 memcpy 行为一致);q后缀指明按 quadword 使用。inc使 r0q 变为 4,dec减回 3,imul乘 5,最终 r0q 为 15。课程提醒:FFmpeg 统一使用小写助记符,大写保留给宏使用。

第 2 课:分支、循环、常量与寻址(lesson_02/index.md)

写完第一个函数后,第 2 课引入控制流与内存寻址。

标签、跳转与 FLAGS

jmp指令无条件跳转,.loop:是标签,点前缀表示局部标签——允许在多个函数中复用同名标签。真实循环依赖 FLAGS 寄存器:除mov外的大多数标量指令(算术、移位等)会根据结果设置 Zero-Flag、Sign-Flag、Overflow-Flag 等标志位,跳转指令据此决策。经典的倒计数循环(lesson_02/index.md):

mov r0q, 3 .loop: ; do something dec r0q jg .loop ; jump if greater than zero

等价于 C 的 do-while:

int i = 3; do { // do something i--; } while(i > 0)

如果要模拟更常见的 for 循环写法(for(i = 0; i < 3; i++)),则需要引入cmp(lesson_02/index.md):

xor r0q, r0q .loop: ; do something inc r0q cmp r0q, 3 jl .loop ; jump if (r0q - 3) < 0, i.e (r0q < 3)

这里有两个关键知识点:

  • xor r0q, r0q是清零寄存器的惯用法,在某些系统上比mov r0q, 0更快(因为不涉及真正的加载);对 SIMD 寄存器可用pxor m0, m0清零整个向量寄存器。
  • cmp实际是"第一个寄存器减第二个寄存器(不保存结果)并设置 FLAGS",配合jl(jump if less)即可实现r0q < 3判断。

课程强调:写汇编不是为了精确复刻 C 循环,而是写出尽可能快的循环。上述 for 风格多了一条cmp指令,指令更少通常意味着更快,所以前面的倒计数循环更受青睐;后续课程还会展示用算术运算顺带设置 FLAGS 来省掉cmp的技巧。

课程还给出了常用跳转助记符速查表(lesson_02/index.md):

助记符含义相关 FLAGS
JE/JZ相等/为零则跳ZF = 1
JNE/JNZ不等/非零则跳ZF = 0
JG/JNLE大于/不小于或等于则跳(有符号)ZF = 0 且 SF = OF
JGE/JNL大于等于/不小于则跳(有符号)SF = OF
JL/JNGE小于/不大于或等于则跳(有符号)SF ≠ OF
JLE/JNG小于等于/不大于则跳(有符号)ZF = 1 或 SF ≠ OF

FLAGS 列仅为完整性列出,写循环时不必深究细节。

常量定义

课程展示了在只读数据段定义常量的方法(lesson_02/index.md):

SECTION_RODATA constants_1: db 1,2,3,4 constants_2: times 2 dw 4,3,2,1
  • SECTION_RODATA是只读数据段宏(不同操作系统的输出文件格式对它的声明方式不同,故用宏统一)。
  • constants_1用db(declare byte)声明,等价于uint8_t constants_1[4] = {1, 2, 3, 4}。
  • constants_2用times 2宏重复声明字,等价于uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1}。

这些标签会被汇编器转换为内存地址,可用于 load(不能用于 store,因为是只读段);某些指令直接接受内存地址作为操作数,无需先显式加载到寄存器(各有取舍)。

偏移与内存寻址

C 编译器会预先算好数组元素间的字节偏移,但手写汇编需要自己计算。通用寻址语法为:

[base + scale*index + disp]
  • base:GPR(通常是 C 函数参数的指针)
  • scale:只能是 1、2、4、8(1 为默认)
  • index:GPR(通常是循环计数器)
  • disp:最大 32 位的整数位移,即数据内的偏移

x86asm 还提供常量mmsize,表示当前所用 SIMD 寄存器的字节大小。一个加载自定义偏移的完整示例(lesson_02/index.md):

;static void simple_loop(const uint8_t *src) INIT_XMM sse2 cglobal simple_loop, 1, 2, 2, src movq r1q, 3 .loop: movu m0, [srcq] movu m1, [srcq+2*r1q+3+mmsize] ; do some things add srcq, mmsize dec r1q jg .loop RET

注意movu m1, [srcq+2*r1q+3+mmsize]中汇编器会自动预计算位移常量。第 3 课会展示一个用单条add替换循环内add+dec组合的技巧。

LEA(Load Effective Address)

理解偏移后就能用lea了。它能用一条指令完成乘法与加法(lesson_02/index.md):

lea r0q, [base + scale*index + disp]

虽然名字叫 "加载有效地址",lea完全可以用于普通算术,例如lea r0q, [r1q + 8*r2q + 5]。它不修改r1q、r2q的内容,也不影响 FLAGS(因此不能基于其输出跳转)。与之对比,用多条指令完成同样计算需要临时寄存器且会污染 FLAGS(因为add会改 FLAGS):

movq r0q, r1q movq r3q, r2q sal r3q, 3 ; shift arithmetic left 3 = * 8 add r3q, 5 add r0q, r3q

lea常被用来在循环前设置地址或做这类计算;注意它不能覆盖所有乘加组合,乘以 1、2、4、8 加上固定偏移是常见用法。

第 3 课:指令集演进与进阶优化技巧(lesson_03/index.md)

x86 指令集演进史

第 3 课先补一段历史课,梳理 x86 SIMD 指令集的演进(lesson_03/index.md,已简化):

指令集发布年份要点
MMX1997Intel 处理器首个 SIMD,64 位寄存器,已成历史
SSE1999128 位寄存器
SSE22000新增大量指令
SSE32004首批水平(horizontal)指令
SSSE32006新增指令,最重要的是pshufb洗牌指令——视频处理中可以说是最重要的指令
SSE42008新增打包最小值/最大值等指令
AVX2011256 位寄存器(仅浮点)与新三操作数语法
AVX22013整数指令的 256 位寄存器
AVX5122017512 位寄存器与新的操作掩码特性;因使用新指令时的 CPU 降频,当时在 FFmpeg 中应用受限;vpermb提供全 512 位洗牌
AVX512ICL2019不再有时钟降频问题
AVX10即将到来—

课程特别提醒:指令集既可能被加入也可能被移除——例如 AVX512 就曾(有争议地)在 Intel 第 12 代 CPU 上被移除。这正是 FFmpeg 做运行时 CPU 检测的原因:FFmpeg 会检测运行机器的 CPU 能力,函数指针默认指向 C 版本,检测到某指令集可用时才替换为对应的汇编变体;检测只做一次,之后不再重复。这与许多专有软件硬编码特定指令集、让功能正常的旧电脑"过时"的做法形成鲜明对比,也使得优化函数可以在运行时开关——这正是开源的一大优势。FFmpeg 还支持仅 SSE 的老机器(约 25 年历史),而 x86inc.asm 能在你用了某指令集不支持的指令时给出提示。

课程引用了 Steam 调查(2024 年 11 月数据,偏游戏玩家群体)展示真实可用性分布(lesson_03/index.md):

指令集可用性
SSE2100%
SSE3100%
SSSE399.86%
SSE4.199.80%
AVX97.39%
AVX294.44%
AVX512(Steam 未区分 AVX512 与 AVX512ICL)14.09%

对 FFmpeg 这种数十亿用户的软件,即便 0.1% 也是庞大的用户群与潜在 bug 报告来源。因此 FFmpeg 的 FATE 测试套件会在数百台机器上运行每一个提交,覆盖 CPU/OS/编译器组合,确保不出问题。

指针偏移技巧

回到第 1 课的add_values,为其增加一个width参数。课程特意使用ptrdiff_t而非int作为宽度类型:直接传int后若按 quadword 做指针运算(widthq),寄存器高 32 位可能被任意值填充;虽然可用movsxd(x86inc.asm 中也有movsxdifnidn宏)符号扩展修复,但用ptrdiff_t更省事。完整示例(lesson_03/index.md):

;static void add_values(uint8_t *src, const uint8_t *src2, ptrdiff_t width) INIT_XMM sse2 cglobal add_values, 3, 3, 2, src, src2, width add srcq, widthq add src2q, widthq neg widthq .loop movu m0, [srcq+widthq] movu m1, [src2q+widthq] paddb m0, m1 movu [srcq+widthq], m0 add widthq, mmsize jl .loop RET

这个技巧的精妙之处在于:

  1. 先把width加到两个指针上,使指针指向缓冲区末尾,然后neg widthq取负;
  2. 加载/存储统一用[srcq+widthq]这样的负偏移——第一次迭代时恰好回到缓冲区开头;
  3. add widthq, mmsize让负宽度逐步趋近零,循环条件用jl(小于零则跳)。

这样widthq同时扮演指针偏移与循环计数器两个角色,省掉了一条cmp指令;而且同一个偏移可用于多处加载和存储,需要时还可以使用偏移的倍数(课程提示这对后续作业很重要)。

对齐

此前示例都用movu回避对齐问题。实际上许多 CPU 在数据对齐时(内存地址能被 SIMD 寄存器大小整除)加载/存储更快,FFmpeg 尽可能用mova做对齐访问。FFmpeg 的av_malloc能在堆上提供对齐内存,C 预处理器指令DECLARE_ALIGNED能在栈上提供对齐内存;若对未对齐地址使用mova,会触发段错误导致程序崩溃。务必确保对齐值与 SIMD 寄存器大小匹配:xmm 为 16、ymm 为 32、zmm 为 64。对齐只读数据段开头的写法:

SECTION_RODATA 64

注意这仅对齐 RODATA 段开头,可能仍需填充字节以保证后续标签保持在 64 字节边界上。

范围扩展(Range expansion)与零扩展

运算中字节值可能溢出 255,此时需要把数据扩展到更大的中间尺寸(如 word)。对无符号字节,用punpcklbw(packed unpack low bytes to words)与punpckhbw(high bytes)。SSE2 版PUNPCKLBW xmm1, xmm2/m128表示源操作数(右侧)可以是 xmm 寄存器或内存地址,目标必须是 xmm 寄存器。

如示意图所示,punpcklbw把两个源寄存器的低半区字节按交错方式合并成字(lesson_03/index.md)。如果源寄存器全为零,则等价于把目标寄存器的字节与零交错——这正是零扩展(字节为无符号)。punpckhbw对高半区字节做同样的事。代码片段:

pxor m2, m2 ; zero out m2 movu m0, [srcq] movu m1, m0 ; make a copy of m0 in m1 punpcklbw m0, m2 punpckhbw m1, m2

执行后m0、m1包含零扩展为 word 的原始字节。课程预告:AVX 的三操作数指令可以省掉这条movu m1, m0复制指令。

符号扩展

有符号数据的范围扩展需要符号扩展:用符号位填充高位。例如 int8_t 的 -2 是0b11111110,符号扩展到 int16_t 时把最高位 1 重复,得到0b1111111111111110。方法是用pcmpgtb(packed compare greater than byte)做0 > byte比较:字节为负则目标字节全 1(0b11111111),否则全 0(0x00000000);再把该结果与字节值交错(lesson_03/index.md):

pxor m2, m2 ; zero out m2 movu m0, [srcq] movu m1, m0 ; make a copy of m0 in m1 pcmpgtb m2, m0 punpcklbw m0, m2 punpckhbw m1, m2

与无符号情形相比多了一条指令。

打包与饱和

packuswb(pack unsigned word to byte)与packsswb把两个包含 word 的 SIMD 寄存器交错打包进一个按字节组织的寄存器,从 word 回到 byte。若值超出 byte 范围会被饱和(saturate),即钳制在最大值处。

洗牌(Shuffles / Permutes)

洗牌被课程称为视频处理中最重要的指令类型,其中 SSSE3 的pshufb(packed shuffle bytes)是最重要的一种:对每个目标字节,用源寄存器对应位置的字节作为目标寄存器的索引;当源字节最高位(MSB)置位时,目标字节被清零。其语义等价于以下 C 代码(在 SIMD 中 16 次循环并行执行)(lesson_03/index.md):

uint8_t tmp[16]; memcpy(tmp, dst, 16); for(int i = 0; i < 16; i++) { if(src[i] & 0x80) dst[i] = 0; else dst[i] = tmp[src[i]]; }

一个简单示例(lesson_03/index.md):

SECTION_DATA 64 shuffle_mask: db 4, 3, 1, 2, -1, 2, 3, 7, 5, 4, 3, 8, 12, 13, 15, -1 section .text movu m0, [srcq] movu m1, [shuffle_mask] pshufb m0, m1 ; shuffle m0 based on m1

-1只是便于阅读的写法:作为字节它是0b11111111(补码),MSB(0x80)置位,因此会清零输出字节。

如何开始学习这门课程

综合 README 与三节课程文档,推荐的学习路径如下:

  1. 确认前置知识:对照 README 的"必备知识"清单,确认自己具备 C 指针与高中数学基础;不熟悉 C 可先补教材。
  2. 按顺序精读课程:第 1 课(汇编与 SIMD 基础、寄存器、x86inc.asm、第一个 SIMD 函数)→ 第 2 课(分支循环、常量、偏移、LEA)→ 第 3 课(指令集历史、指针偏移技巧、对齐、范围/符号扩展、打包与洗牌)。每课结尾有 "Next Lesson" 指引(第 2 课末尾指向 第 3 课),第 1 课末尾指向 第 2 课,衔接清晰。
  3. 边学边对照真实工程:课程反复出现的x86inc.asm、cglobal、INIT_XMM、movu/mova、mmsize、函数指针替换机制等,正是 x264、FFmpeg、dav1d 三大项目实际使用的编程范式;课程引用的 FATE 测试体系、运行时 CPU 检测策略也直接来自 FFmpeg 工程实践。
  4. 注意当前限制:作业(assignments)尚未上传到仓库;第 1 课第 3 课明确提到作业会要求"创建函数指针指向汇编函数""加载常量并在循环中加到 SIMD 向量上",这些实践可等待作业上线后完成。课程目前聚焦 x86 64 位(amd64,Intel 与 AMD CPU 均适用)的 Intel 语法汇编,ARM、RISC-V 等架构可能在未来扩展。
  5. 善用社区与参考资料:学习中有疑问可在课程提供的 Discord 服务器提问;课程还推荐了补充资料,包括《The Art of 64-bit Assembly》中可视化展示 SIMD 指令行为的图解、Intel 官方指令集手册及其非官方网页版检索工具、以及可视化的 SIMD 指令参考网站(详见课程文档)。课程也坦诚地提醒:由于课程采用 Intel 语法手写汇编且聚焦高性能图像处理,Stack Overflow 等通用资源未必比得上这些针对性材料——这也是这门课的独特价值所在。

完成这三节课后,你应当已经理解:为什么 FFmpeg 要手写 SIMD 汇编、如何用x86inc.asm编写一个标准汇编函数、如何用循环与寻址处理连续内存、如何利用指令集特性(对齐、零/符号扩展、打包、洗牌)写出高性能图像处理代码——这正是为 FFmpeg 贡献代码所需的第一步。

  • 教程
  • 文档

【免费下载链接】asm-lessons

FFmpeg Assembly Language Lessons

项目地址:https://gitcode.com/GitHub_Trending/as/asm-lessons
点击查看免费下载
上一篇:Sefirah-Android剪贴板共享完全教程:实现跨设备复制粘贴的10个技巧
下一篇:如何用Awesome Claude Skills实现Excel自动化数据分析的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询