程序猿之编译原理 第6章 中间代码生成 6.1 语法树的变体

编译原理 — 第6章 中间代码生成

在编译器的分析与综合模型中,前端将源程序进行分析并生成中间表示,后端则在此基础上生成目标代码。在理想情况下,前端生成的中间表示能屏蔽目标机器的结构差异,使得后端只需根据该中间表示进行适配即可。因此,构造合理的中间代码表示,是前后端解耦的重要基础。

图6-1 展示了一个典型的编译器前端的处理过程:词法分析器生成记号流,语法分析器据此构建语法树,静态语义检查模块完成类型等一致性校验,最终交由中间代码生成器构造中间表示。为了提高效率,语义分析和中间代码生成往往合并进行。

图6-1 一个编译器前端的逻辑结构
词法分析 → 语法分析 → 静态检查 → 中间代码生成 → 中间代码

中间代码的设计目标是表达程序逻辑,同时方便后端代码生成与优化,因此通常采用“低级抽象”的中间语言形式。例如三地址码就是一种典型中间表示,它的每一条指令最多涉及两个操作数和一个结果变量,结构统一、便于分析。

图6-2 展示了不同中间表示的层级划分,从高层的语法树、抽象语法树,到低层的三地址码等,逐层逼近机器语言的形式。

图6-2 编译器可能使用的一系列中间表示
高层 IR → 中层 IR → 低层 IR → 目标代码


6.1 语法树的变体

编译器前端常用语法树表示程序结构,但为了去除冗余、便于共享子表达式,引入了无环有向图(DAG)作为语法树的变体,用于表示表达式中的计算过程。

6.1.1 表达式的语法树和DAG图

在语法树中,每个内部结点表示操作符,每个叶子节点表示操作数。而 DAG 的特点是允许多个节点共享同一子树,用于优化如 a * (b - c) + (b - c) * d 这样的表达式,避免重复计算。

图6-3 展示了该表达式的语法树转换为 DAG 后的结构,显著减少了冗余结点。

图6-3 表达式 a + a * (b - c) + (b - c) * d 的 DAG

  • 重复的子表达式 (b - c) 被共享

  • 节点“a”仅出现一次

6.1.2 构造 DAG 的结点编号方法

为了构建 DAG,需为每个唯一子表达式分配唯一编号。图6-4 列出了一套用于构造 DAG 的语法制导定义(SDD),其中使用 Node 和 Leaf 函数为表达式树生成结点。

图6-4 生成语法树或 DAG 的语法制导定义

  • T → num:创建叶节点

  • E → E1 + T:创建内部节点,左子树为 E1,右子树为 T

图6-5 则展示了构建一个表达式的各个步骤,包括查找共享节点、构建新节点并分配编号等过程。

图6-5 构造 DAG 的步骤

  • 第一步:Leaf(a) → p1

  • 第二步:Leaf(b) → p2

  • 第三步:Leaf(c) → p3

  • 第十三步:Node('+', p7, p12) → p13

6.1.2 构造 DAG 的结点编号方法

为了实现编号分配,需将节点信息编码为三元组 <op, l, r>,其中 op 是运算符,lr 是左右子结点的编号。图6-6 展示了如何将 i = i + 10 表达式转换为一个包含叶节点和内部节点的 DAG 编码结构。

图6-6 i = i + 10 的 DAG 结构及其编码

  • 编号 1:Leaf(i)

  • 编号 2:Leaf(10)

  • 编号 3:Node('+', 1, 2)

此外,为了避免冗余节点,我们采用查重策略,即在构造 DAG 时首先检查是否已有结点匹配当前 <op, l, r>,若存在则复用,否则创建新结点。该过程可通过数组、链表或哈希表实现。


6.1.3 使用散列表构造 DAG

散列表是一种高效的查找结构,适合用于判断 <op, l, r> 是否已存在于当前 DAG 中。图6-7 展示了如何使用散列表将三元组映射到桶,并在桶中通过链表查找是否已有匹配结点。

图6-7 用于搜索桶的散列表结构

  • 每个桶保存 <op, l, r> 的单元

  • 冲突通过链表法解决

  • 典型哈希函数:h(op, l, r)

散列表可显著提高查找效率,尤其在表达式中存在大量公共子表达式时,能够避免重复构造,大幅降低中间代码生成的时间复杂度。


6.1.3 小节练习

  • 练习6.1.1:为表达式 ((x+y)-((z-y))*((x+y)-(z-y))) 构造 DAG;

  • 练习6.1.2:为表达式 a+b+(a+b)a+b+a+ba+a+(a+a+(a+a+(a+a))) 构造 DAG,标出公共子表达式的共享结构。


练习 6.1.1

为表达式 ((x + y) – ((z – y) * ((x + y) – (z – y)))) 构造 DAG

graph TD
    %% 叶子
    X[x]  &nbsp;
    Y[y]  &nbsp;
    Z[z]  &nbsp;

    %% 公共子表达式
    N1((x + y))
    N2((z - y))

    %% 依赖 N1、N2 的子表达式
    N3(((x + y) - (z - y)))
    N4(((z - y) * ((x + y) - (z - y))))

    %% 根
    R(((x + y) - N4))

    %% 边
    X --> N1
    Y --> N1
    Z --> N2
    Y --> N2
    N1 --> N3
    N2 --> N3
    N2 --> N4
    N3 --> N4
    N1 --> R
    N4 --> R

说明

  • N1x + y)和 N2z - y)分别被后续两处引用,DAG 只保留一份结点;

  • N3N4 依赖 N1N2,根结点 R 最终组合整个表达式。


练习 6.1.2

(1) 表达式 a + b + (a + b)

graph TD
    A[a] &nbsp;
    B[b] &nbsp;

    AB1((a + b))
    R1((AB1 + AB1))

    A --> AB1
    B --> AB1
    AB1 --> R1
    AB1 --> R1

公共子表达式:唯一的 (a + b) 结点 AB1 被根结点 R1 复用两次。


(2) 表达式 a + b + a + b

(按左结合记作 (((a + b) + a) + b)

graph TD
    A2[a] &nbsp;
    B2[b] &nbsp;

    AB2((a + b))
    S2((AB2 + a))
    R2((S2 + b))

    A2 --> AB2
    B2 --> AB2
    AB2 --> S2
    A2 --> S2
    S2 --> R2
    B2 --> R2

公共子表达式:虽然两处出现 a + b,但只有第一次按左结合真正形成子树,后续出现的 ab 属于不同分支,故 共享第二次的 a + b;DAG 仍旧只有一个 AB2 结点。


(3) 表达式 a + a + (a + a + (a + a + (a + a)))

(可视为 ((a + a) + ( (a + a) + ( (a + a) + (a + a) ) ) )

graph TD
    A3[a] &nbsp;

    AA((a + a))     %% 唯一公共子表达式
    L1((AA + AA))   %% 左侧:a+a + a+a
    L2((AA + L1))   %% 依次嵌套
    R3((AA + L2))   %% 根结点

    A3 --> AA
    A3 --> AA
    AA --> L1
    AA --> L1
    AA --> L2
    L1 --> L2
    AA --> R3
    L2 --> R3

公共子表达式

  • 所有出现的 a + a 仅保留单个结点 AA

  • AA 通过多条边被不同父结点引用,显式展示了 DAG 的“共享”效果。

理解

一、理论理解:语法树是结构,DAG 是优化

在编译原理中,语法树是用于表示源程序语法结构的一种基本数据结构,其节点构成层级表示程序中各个子表达式之间的嵌套关系,而静态语义检查与中间代码生成过程往往以语法树为核心展开。然而,随着表达式复杂度提升,语法树也不可避免地重复出现同一个子表达式的结构。例如,在表达式 ((x + y) – ((z – y) * ((x + y) – (z – y)))) 中,子表达式 (x + y)(z – y) 各自出现了两次,如果直接使用语法树,势必构建两个完全相同的子树,导致冗余。

此时引入 DAG(Directed Acyclic Graph,有向无环图)作为语法树的“结构变体”,其本质目标是共享重复子表达式的结构节点,从而在数据结构上压缩空间、减少指令生成次数,并为后端生成三地址码提供潜在优化机会。教材中引入了语法制导翻译(SDD)配合 DAG 构造规则,使得每个公共子表达式仅生成一个唯一节点,从根源上控制结构膨胀。再配合编号、查重与哈希表机制,DAG 的构造逻辑可以高度自动化且高效。

从练习题角度来看:表达式 a + b + (a + b)a + b + a + b 尽管结构相近,但 DAG 是否能共享节点依赖于解析顺序与结合方式,说明 DAG 不仅是一种结构,也是一种优化的思维模型:它必须遵循语义一致的同时,还要尽可能压缩表示。


二、大厂实战理解:DAG 是工业编译器中“共享表达”的第一道防线

在工业级编译器、解释器乃至 JVM/LLVM 等现代语言运行环境中,表达式 DAG 不仅是语法树的压缩版,更是中间代码生成过程中启用优化策略的“入口点”。以 LLVM 为例,其 IR(中间表示)在早期会对 SSA 表达式树进行“Common Subexpression Elimination(CSE)”,其内部正是基于 DAG 的共享节点模型。在 Google 的 V8 JavaScript 引擎中,TurboFan 编译器通过构建“节点图”,对所有表达式计算路径进行 DAG 化,以支持后续的 lazy deoptimization 与代码内联优化。

又如,腾讯自研的 Lua 虚拟机 LuaJIT 在 JIT 编译阶段会构建 IR DAG 来识别哪些子表达式可以复用并用寄存器保持,从而显著减少了寄存器压栈操作次数,提升运算密度。在 AI 编译器如 ONNXRuntime、TensorRT 中,表达式 DAG 甚至会被拓展为 Operator Graph(算子图),并作为调度图、内存分配图的输入,说明 DAG 思维已经深入大厂底层。

回到练习题本身,如 a + b + (a + b)a + b + a + b,在大厂的中间代码生成器中会被统一转换为 DAG 并进行值编号处理,再依据编号图构建最优执行顺序,这也是现实中的 CSE Pass 模块所做的事情。


三、自研编译器视角:没有 DAG,你的表达式翻译就全是重复劳动

当你尝试自己从零编写一个简易编译器,假设你已经实现了词法分析和语法分析模块,并能通过语法树表示简单表达式,此时你会发现,如果不进行子表达式共享,那么 x + y + z + (x + y) 这样表达式的语法树会呈现指数级膨胀 —— 所有重复结构都被硬编码为一棵棵“独立而雷同”的子树。

当你开始写一个中间代码生成器,比如尝试输出三地址码(如 t1 = x + y; t2 = z + t1; t3 = x + y; t4 = t2 + t3),你会突然意识到你竟然重新生成了完全一样的指令序列,而这些指令只是因为你没能在结构层识别 (x + y) 已经存在而导致的“冗余翻译”。这正是你必须引入 DAG 编码的动因。

你可能会像教材中那样,引入 Node(op, l, r) 三元组,并开始建立散列表 h(op, l, r) 来快速判断当前表达式结构是否存在已有结点。随着你实现了编号复用,你会欣喜地发现中间代码不仅变短了,而且你开始能思考“值编号表”、“公共表达式消除”、“局部代码移动”等高级优化策略。

最终你会明白:DAG 是你编译器中第一步能让“结构”指导“生成”的地方,它是编译器的第一个智能判断机制。

面试题 1:什么是 DAG?它和语法树的本质区别是什么?

参考回答:
DAG(有向无环图)是一种比语法树更紧凑的表达式结构,它允许多个操作共享同一子表达式的节点,以减少冗余计算和结构重复;而语法树则是一种标准的树形结构,不能出现子结构共享,因此当表达式中存在公共子表达式时,语法树会多次重复该子树,而 DAG 则只保留一次,并通过多个边共享;例如在表达式 a + b + (a + b) 中,DAG 会复用 (a + b) 的节点,而语法树会构造两个相同的子树;本质上,DAG 是语法树的最小化变体,是为表达式优化而引入的结构。


面试题 2:请简要描述一个 DAG 构造过程中如何避免生成重复子表达式节点?

参考回答:
为了避免重复子表达式节点的构造,DAG 的构造过程通常需要引入节点查重机制,即在每次生成一个 <op, l, r> 形式的操作结点之前,先在已存在的节点集合中查找是否有同样的三元组存在;如果存在就直接复用已有节点,否则创建新节点;这一过程可以通过散列表实现,哈希函数以 op, left, right 三个字段为输入,定位到对应桶,并在线性链表中进行匹配;例如在 Node('+', p1, p2) 创建前,必须查询 h('+', p1, p2) 是否存在已有项,存在则复用,确保每个公共子表达式仅生成一次节点,从而实现结构压缩与表达共享。


面试题 3:DAG 构造为什么是中间代码优化的前提条件?它对三地址码生成有何帮助?

参考回答:
DAG 构造是中间代码优化的前提条件,是因为它可以识别并共享表达式中的公共子结构,在结构层面实现去冗余,从而为后续生成三地址码时消除重复指令提供可能;例如若表达式 (x + y) 出现两次,构造语法树会产生两段 t1 = x + y; t2 = x + y;,而 DAG 会只生成一次 t1 = x + y; 并复用该临时变量;因此 DAG 能直接降低中间代码长度、优化寄存器使用、提升执行性能,同时它也为常见优化 Pass,如公共子表达式消除(CSE)、局部代码移动(LCM)等提供结构基础,是优化型编译器中不可或缺的组成部分。


面试题 4:请举例说明 DAG 在现代工业编译器(如 LLVM、V8、JVM)中的实际应用场景

参考回答:
在 LLVM 编译器中,SSA 表达式的构造过程实质上即是一个带有值编号的 DAG 图,它能够通过 Value Numbering 和 CSE Pass 去除冗余表达式;在 Google 的 V8 引擎中,TurboFan 生成的中间表示(IR)也是 DAG 结构,通过共享节点支持优化路径选择、lazy deoptimization 等高级特性;而在 Java 虚拟机的 JIT 编译器中,HotSpot 通过构造表达式 DAG 来判断哪些计算可以使用常量传播、表达式预计算等优化手段;可见 DAG 并非教学模型,而是工业编译器中表达优化与代码生成调度的核心数据结构。


面试题 5:在表达式 a + b + (a + b)a + b + a + b 中,哪一个能更好地利用 DAG 共享结构?请解释原因。

参考回答:
表达式 a + b + (a + b) 更能充分利用 DAG 的结构共享能力,因为 (a + b) 作为公共子表达式在语义上与前缀 a + b 完全一致,结构也完全相同,因此在 DAG 中只会构造一个节点,并在后续被两次引用;而 a + b + a + b 虽然表面上也包含两个 (a + b),但由于表达式是左结合的,其解析结构为 (((a + b) + a) + b),其中 ab 出现在不同子树中,因此无法识别为完全相同的公共子结构,故 DAG 无法共享结点,最终构造出的 DAG 也会更大;这个对比说明 DAG 的结构共享取决于表达式的解析结构和语义一致性。

场景题 1:你在字节跳动架构组负责自研中间代码 IR 表达层设计,发现某个 AI 推理 DSL 编译器在生成中间代码时出现大量重复的临时变量计算,导致后端三地址码数量激增、寄存器压力过高,影响执行效率,请你从表达结构角度分析优化路径。

参考回答:
面对中间代码阶段临时变量激增的问题,我首先会从表达式结构入手分析中间表示是否存在大量语义重复但结构分离的表达式,例如 (a + b) + (a + b) 或多个语句中复现 conv2d(weight, x) 这样的模式;一旦确认这些重复并非真实的语义差异,而是结构重复导致的冗余,我会考虑将当前的语法树表示转换为 DAG 表达式图,从结构上合并具有相同三元组 <op, l, r> 的子表达式;在实现上,我会通过建立值编号表或三元组哈希表来实现节点查重,在构造中间表达式树时优先判断已有结构是否已经存在,并进行引用复用而非重复构造;这一机制可以显著减少后端临时变量数量,降低指令冗余和寄存器冲突,同时也为后续优化 Pass(如 CSE、GCM)提供数据基础,从而有效提升整个中间代码执行效率与缓存命中率。


场景题 2:你在阿里 PAI 平台团队中负责图计算图优化模块,发现某些用户表达式图在生成物理执行计划时结构冗余严重,多个相同子表达式被多次调度执行,导致性能线性下降,请问你如何改造表达表示以减少冗余调度?

参考回答:
针对图计算执行计划中冗余子表达式重复调度的问题,我会从表达式图的 DAG 化入手,对用户 DSL 编译生成的语法树结构进行语义层去重与结构层合并;具体而言,我会引入表达式节点唯一标识符(如 <op, input1_id, input2_id>)并建立散列表缓存,构造过程中每遇到一个表达式节点,就查询是否已存在等价结构,若存在则直接引用;这样可以使得如 x = (f + g) + (f + g) 只生成一个 f + g 节点,并复用其结果,避免下游计划重复调度;同时我会设计 DAG 上层的执行次数追踪机制,只有确实存在多处语义路径需要结果时,才允许结果 materialize 为中间变量;这种 DAG 结构可在逻辑层提前进行公共子表达式提取,进而简化底层算子图和调度计划,实现从 DSL 到执行计划层的结构裁剪与重复消除。


场景题 3:你在 Google 编译团队负责 TensorFlow XLA 的表达式归约优化模块,某模型的 JIT 编译输出中存在大量重复的 matmul + bias + relu 结构,而这些结构由于语法树合并失败未能识别为公共表达式,请问你如何在表达式表示层解决这一问题?

参考回答:
面对 JIT 编译器无法识别 matmul + bias + relu 等表达式为公共子表达式的情况,我会首先从语法树结构角度排查是否是因为微小语义差异(如参数命名、类型注释)或节点结构顺序不一致,导致语法树构建时视为不同节点;为解决此问题,我会重构表达式表示结构,将原先基于递归树结构的表达式语义封装为标准的 DAG 图,在构建阶段引入“语义哈希”(semantic hash)机制,对每个操作节点计算其 op-code、输入依赖 ID、操作类型等的结构化签名,并通过散列查找判断是否可复用;在构建 DAG 的过程中,只要识别出完全等价的三元组签名,就使用共享节点代替新建节点,从而实现表达式级别的结构压缩;最终这将使得多个 matmul + bias + relu 表达式路径在中间层归并为一个共享节点,从而有效消除重复代码、减少临时张量生成、优化缓存访问与执行时间。


场景题 4:你正在从零开发一个教学用编译器 miniC,构建中间表示阶段发现如果使用语法树则在表达式 ((x+y) - ((z-y)*((x+y)-(z-y)))) 中生成了多个重复的子树,三地址码冗余,结构冗长,你如何引入 DAG 表示解决该问题?请描述你如何实现节点构建与查重。

参考回答:
在 miniC 编译器中面对重复子表达式导致语法树结构臃肿的问题,我会引入 DAG(有向无环图)作为表达式的中间结构表示,用于从语法结构层面合并公共子表达式,进而减少后续三地址码冗余生成;我会定义 DAG 节点为 <op, left, right> 三元组,每个表达式节点创建前先通过哈希函数计算该三元组的哈希值,并在全局哈希表中查找是否已存在对应结构;如果存在,即复用已有节点编号;否则生成新节点并写入表中;举例而言,在表达式 ((x+y)-(z-y))*((x+y)-(z-y)) 中,子表达式 (x+y)(z-y) 会被识别并复用一次,最终构造出的 DAG 结构紧凑清晰,三地址码只生成一次 (x+y)(z-y) 的临时变量,并在多个地方复用,从而减少了临时变量数量与执行指令数量,优化了表达式翻译效率。

场景题 5:你在一家大模型基础设施团队负责自研 AI 操作系统(AIOS)的 IR 图调度与算子融合模块,系统通过 IR 表达图调度模型执行流程,但你发现模型中大量的 Linear + ReLU + Dropout 组合在不同 batch 中被重复构建,导致图结构冗余、内存占用上升、图调度效率下降。请问你如何借助 DAG 思维改造表达结构,以提升系统级调度性能?

参考回答:
在自研 AI 操作系统中,我首先明确系统中每一次模型前向过程所构建的 IR 表达图是否存在节点级别的语义冗余,即多个 batch 中调用结构完全相同但内存节点与图节点各自独立的 Linear + ReLU + Dropout 模块;这些冗余构建不仅增加了图结构大小,还拉高了内存使用峰值与调度图构建时间。为解决这一问题,我会从 DAG 的表达共享角度切入,尝试将当前的图构建流程从“语法树式拼图”模式升级为“全局 DAG 结构构建”模式,即在图构建阶段不再每次都新建节点,而是通过引入语义归一化(operator fingerprinting)与值编号查重机制,确保结构等价的表达子图只在 DAG 中出现一次并被引用多次。

具体实现上,我会将每个节点视为三元组 <op, input_ids, attributes>,并对其哈希进行缓存查找判断是否复用,且引入全局表达子图缓存池;同时我还会使用 DAG 节点生命周期控制策略,确保引用计数为 1 的节点才可回收,避免内存滥用;这种 DAG 化图结构一方面能使得图调度器在执行阶段通过拓扑排序获得更紧凑的执行路径,另一方面也可减少模型中多实例重复构建时带来的 GraphCompile 开销;最终,整个 AIOS 系统在高并发推理场景下将获得更低的调度延迟与更高的 operator 重用率。

景题 6(自研 AI 操作系统方向)

你在自研 AI 操作系统(AIOS)研发团队担任中间表达系统架构负责人,目标是构建一个支持算子图调度、编译优化、异构执行的编译型 AI 系统,从前端 DSL 到后端设备,需要设计一套中间表达结构。请你描述从 0 到 1 搭建该表达式构造系统的全过程,重点说明为什么从语法树演进到 DAG 是必要的阶段,以及你是如何实现结构优化与执行调度解耦的。


参考回答:
在从零构建自研 AI 操作系统(AIOS)过程中,表达式建图模块是系统前端与后端之间的“翻译器”,承担着从 DSL(如 PyTorch、Mojo 或 DSL AST)到调度图(Scheduling DAG)的关键桥梁角色。起初我们仅以语法树(AST)为基础构造计算图,但很快在真实模型场景中发现语法树存在三个致命问题:其一,模型层之间公共子表达式无法复用,导致图中结构冗余、构建效率低下;其二,语法树本身不支持表达非树形的数据依赖结构,例如多个算子依赖同一节点输出;其三,语法树不能表达优化机会(如算子融合、冗余消除),不适合进入调度与代码生成流程。

因此我主导将表达式结构升级为 DAG(有向无环图)模型,使得每个表达式节点可以共享结构、追踪依赖,并天然支持子图复用与流图优化。在设计中,我们为每个节点设计了 <op_type, input_ids, attrs> 三元组签名,构造时通过语义哈希(semantic hash)和全局值编号表进行查重;若发现结构完全一致、输入一致、参数一致的表达式节点,则仅构建一个 DAG 节点并为多个上层节点提供引用。这使得像 matmul + bias + relu 这类在多 batch、多个子模块中复用的表达子图只在 DAG 中存在一次,极大压缩了 IR 空间,提高了调度局部性。

同时我构建了 DAG-to-IR 编译模块,将逻辑 DAG 编译为设备无关中间表示(如 Triton-IR 或 MindIR),在后续可映射至 CPU、GPU、NPU 等后端执行单元;DAG 本身则作为调度器的数据结构支撑执行图拓扑排序、批量融合、异步调度等优化过程。更重要的是,DAG 的共享结构还支撑了我们对表达式进行等价变换(Rewrite Rule)、恒等折叠(Constant Folding)和数据重排(Data Layout Transformation)等编译级优化。

最终,这一套从语法树演进至 DAG,再构建 IR 与调度图的架构,使得我们从一个仅能表达推理逻辑的原型系统,发展为可以执行多模型融合、异构调度、流图切分与静态图编译的完整 AIOS 平台。可以说,DAG 的引入不是一个优化技术点,而是一个系统架构能力的分水岭,直接决定了我们 AI 操作系统从 DSL 到执行图的表达能力和可扩展性。


结论

  1. 通过 DAG 共享公共子表达式,可消除冗余运算,降低后端生成三地址码的指令数量。

  2. a + b + (a + b)a + b + a + b 虽然在文本上看似重复,但结合规则不同,导致 DAG 的共享效果差异明显。

  3. 在重度重复表达式(如大量 a + a)中,共享结点显著减少节点数与后续优化成本,这正是 DAG 在中间代码生成阶段的重要意义。


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值