LLVM学习笔记(6)

2.2.4.2. 可复用的结构

2.2.4.2.1. PatFrag

高级语言的特征之一是支持数据与结构的复用,TD语言也吸取了这些长处,尝试为复杂而基本的操作提供复用的可能性。这就是PatFrag(SelectionTargetDAG.td):

606     class PatFrag<dag ops, dag frag, code pred = [{}],

607                   SDNodeXForm xform = NOOP_SDNodeXForm> : SDPatternOperator {

608       dag Operands = ops;

609       dag Fragment = frag;

610       code PredicateCode = pred;

611       code ImmediateCode = [{}];

612       SDNodeXForm OperandTransform = xform;

613     }

在嵌入上一级结构时,PatFrag就代表了Fragment所指定的片段,而Operands用作该片段的操作数。PredicateCode则是所谓的谓词,它是一段嵌入生成的指令选择器的代码,只有满足这段代码所代表的条件,才会使用这个PatFrag。TableGen将在适当时把PatFrag展开为Fragment指定的片段,并使用Operands所指定的操作数替换片段中的操作数。

PatFrag也有文档没有给出的约束。首先,参数ops只能是以ops、ins、outs为操作符的dag,而操作数必须是node:$name的形式。其次,参数frag也只能使用特定的TD定义作为操作符,以node:$name或?:name的形式引用PatFrag的操作数,而且必须援引所有的操作数。

V7.0大幅修改了PatFrag的定义:

689     class PatFrag<dag ops, dag frag, code pred = [{}],

690                   SDNodeXForm xform = NOOP_SDNodeXForm>

691       : PatFrags<ops, [frag], pred, xform>;

引入了PatFrags,它的Fragments部分(627行)是一个匹配片段列表。每个片段可以匹配DAG上某个东西,从单个节点到其他多个嵌套的片段。如果任一片段匹配那么整个片段组就匹配了。这使得使用同一个片段组来匹配“带溢出加法”与“普通加法”成为可能。

639行开始的域也会用于生成谓词(生成TreePredicateFn的成员方法)。这里谓词分为3类:loadstoreatomicIsLoadIsStoreIsAtomic分别用于表示是否使用这些谓词。方法TreePredicateFn::getPredCode()将检查它们间的合理性与合法性。

624     class PatFrags<dag ops, list<dag> frags, code pred = [{}],

625                    SDNodeXForm xform = NOOP_SDNodeXForm> : SDPatternOperator {

626       dag Operands = ops;

627       list<dag> Fragments = frags;

628       code PredicateCode = pred;

629       code GISelPredicateCode = [{}];

630       code ImmediateCode = [{}];

631       SDNodeXForm OperandTransform = xform;

632    

633       // Define a few pre-packaged predicates. This helps GlobalISel import

634     // existing rules from SelectionDAG for many common cases.

635       // They will be tested prior to the code in pred and must not be used in

636       // ImmLeaf and its subclasses.

637    

638       // Is the desired pre-packaged predicate for a load?

639       bit IsLoad = ?;

640       // Is the desired pre-packaged predicate for a store?

641       bit IsStore = ?;

642       // Is the desired pre-packaged predicate for an atomic?

643       bit IsAtomic = ?;

644    

645       // cast<LoadSDNode>(N)->getAddressingMode() == ISD::UNINDEXED;

646       // cast<StoreSDNode>(N)->getAddressingMode() == ISD::UNINDEXED;

647       bit IsUnindexed = ?;

648    

649       // cast<LoadSDNode>(N)->getExtensionType() != ISD::NON_EXTLOAD

650       bit IsNonExtLoad = ?;

651       // cast<LoadSDNode>(N)->getExtensionType() == ISD::EXTLOAD;

652       bit IsAnyExtLoad = ?;

653       // cast<LoadSDNode>(N)->getExtensionType() == ISD::SEXTLOAD;

654       bit IsSignExtLoad = ?;

655       // cast<LoadSDNode>(N)->getExtensionType() == ISD::ZEXTLOAD;

656       bit IsZeroExtLoad = ?;

657       // !cast<StoreSDNode>(N)->isTruncatingStore();

658       // cast<StoreSDNode>(N)->isTruncatingStore();

659       bit IsTruncStore = ?;

660    

661       // cast<AtomicSDNode>(N)->getOrdering() == AtomicOrdering::Monotonic

662       bit IsAtomicOrderingMonotonic = ?;

663       // cast<AtomicSDNode>(N)->getOrdering() == AtomicOrdering::Acquire

664       bit IsAtomicOrderingAcquire = ?;

665       // cast<AtomicSDNode>(N)->getOrdering() == AtomicOrdering::Release

666       bit IsAtomicOrderingRelease = ?;

667       // cast<AtomicSDNode>(N)->getOrdering() == AtomicOrdering::AcquireRelease

668       bit IsAtomicOrderingAcquireRelease = ?;

669       // cast<AtomicSDNode>(N)->getOrdering() == AtomicOrdering::SequentiallyConsistent

670       bit IsAtomicOrderingSequentiallyConsistent = ?;

671    

672       // isAcquireOrStronger(cast<AtomicSDNode>(N)->getOrdering())

673       // !isAcquireOrStronger(cast<AtomicSDNode>(N)->getOrdering())

674       bit IsAtomicOrderingAcquireOrStronger = ?;

675    

676       // isReleaseOrStronger(cast<AtomicSDNode>(N)->getOrdering())

677       // !isReleaseOrStronger(cast<AtomicSDNode>(N)->getOrdering())

678       bit IsAtomicOrderingReleaseOrStronger = ?;

679    

680       // cast<LoadSDNode>(N)->getMemoryVT() == MVT::<VT>;

681       // cast<StoreSDNode>(N)->getMemoryVT() == MVT::<VT>;

682       ValueType MemoryVT = ?;

683       // cast<LoadSDNode>(N)->getMemoryVT().getScalarType() == MVT::<VT>;

684     // cast<StoreSDNode>(N)->getMemoryVT().getScalarType() == MVT::<VT>;

685       ValueType ScalarMemoryVT = ?;

686     }

2.2.4.2.1.1. SNDodeForm

SDNodeXForm是一个特别的定义。它允许目标机器在匹配的情形下修改输出操作数(在输出dag里),这通常用于修改立即数。

574     class SDNodeXForm<SDNode opc, code xformFunction> {

575       SDNode Opcode = opc;

576       code XFormFunction = xformFunction;

577     }

XFormFunction给出了进行操作的代码片段,Opcode则是操作的对象。

上面607行的NOOP_SDNodeXForm用于表示空操作:

579     def NOOP_SDNodeXForm : SDNodeXForm<imm, [{}]>;

另一个来自X86机器的例子则是:

851     def ROT32L2R_imm8: SDNodeXForm<imm, [{

852       // Convert a ROTL shamt to a ROTR shamt on 32-bit integer.

853       return getI8Imm(32 - N->getZExtValue(), SDLoc(N));

854     }]>;

TableGen在生成后端代码时,会将853行的代码片段嵌入合适的地方,以对指定的操作数进行指定的处理。

2.2.4.2.1.2. PatFrag的例子

以下是X86机器中PatFrag的一种派生定义(v7.0的定义是一致的):

889     def loadi16 : PatFrag<(ops node:$ptr), (i16 (unindexedload node:$ptr)), [{

890       LoadSDNode *LD = cast<LoadSDNode>(N);

891       ISD::LoadExtType ExtType = LD->getExtensionType();

892       if (ExtType == ISD::NON_EXTLOAD)

893         return true;

894       if (ExtType == ISD::EXTLOAD)

895         return LD->getAlignment() >= 2 && !LD->isVolatile();

896       return false;

897     }]>;

这个结构描述的是载入16位有符号整数的操作。其中unindexedload也是一个PatFrag派生定义(所谓的unindexed就是实际地址已经算出,合并在基址指针里):

674     def unindexedload : PatFrag<(ops node:$ptr), (ld node:$ptr), [{

675       return cast<LoadSDNode>(N)->getAddressingMode() == ISD::UNINDEXED;

676     }]>;

V7.0的定义是这样的:

781     def unindexedload : PatFrag<(ops node:$ptr), (ld node:$ptr)> {

782       let IsLoad = 1;

783       let IsUnindexed = 1;

784     }

上面675行的谓词将由TreePredicateFn::getPredCode()方法根据782~783行的设置自动生成。

而在这个定义里,替换片段ld也是一个SDNode派生定义(v7.0的定义是一致的):

505     def ld: SDNode<"ISD::LOAD", SDTLoad,

506                             [SDNPHasChain, SDNPMayLoad, SDNPMemOperand]>;

因为枚举值ISD::LOAD,ld匹配SelectionDAG的LoadSDNode。

loadi16的操作数是一个dag值,ops用于向TableGen说明其参数就是一个操作数。在TableGen看来,这三个定义使用的操作数是同一个,因为所有的操作数都叫“ptr”。

这个PatFrag在TableGen展开后,是这样的(在llvm的源代码根目录,输入:

$ llvm-tblgen lib/Target/X86/X86.td -I=include -I=lib/Target/X86

就可以得到仅包含简单def的输出——这相当于C/C++源代码的宏展开。TableGen并不使用这个结果,这只是为了让开发人员更好地查看、调试这些定义):

def loadi16 {    // SDPatternOperator PatFrag

  SDNodeXForm PatFrag:xform = NOOP_SDNodeXForm;

  dag Operands = (ops node:$ptr);

  dag Fragment = (i16 (unindexedload node:$ptr));

  string PredicateCode = "

  LoadSDNode *LD = cast<LoadSDNode>(N);

  ISD::LoadExtType ExtType = LD->getExtensionType();

  if (ExtType == ISD::NON_EXTLOAD)

    return true;

  if (ExtType == ISD::EXTLOAD)

    return LD->getAlignment() >= 2 && !LD->isVolatile();

  return false;

";

  string ImmediateCode = "";

  SDNodeXForm OperandTransform = NOOP_SDNodeXForm;

  string NAME = ?;

}

注意loadi16是PatFrag的派生定义,虽然展开结果没有明确指出,但TableGen知道它是一个PatFrag实例,并在处理中记录了这个信息。

V7.0上的结果是:

def loadi16 {   // SDPatternOperator PatFrags PatFrag

  list<SDNodeProperty> Properties = [];

  dag Operands = (ops node:$ptr);

  list<dag> Fragments = [(i16 (unindexedload node:$ptr))];

  code PredicateCode = [{

  LoadSDNode *LD = cast<LoadSDNode>(N);

  ISD::LoadExtType ExtType = LD->getExtensionType();

  if (ExtType == ISD::NON_EXTLOAD)

    return true;

  if (ExtType == ISD::EXTLOAD)

    return LD->getAlignment() >= 2 && !LD->isVolatile();

  return false;

}];

  code GISelPredicateCode = [{}];

  code ImmediateCode = [{}];

  SDNodeXForm OperandTransform = NOOP_SDNodeXForm;

  bit IsLoad = ?;

  bit IsStore = ?;

  bit IsAtomic = ?;

  bit IsUnindexed = ?;

  bit IsNonExtLoad = ?;

  bit IsAnyExtLoad = ?;

  bit IsSignExtLoad = ?;

  bit IsZeroExtLoad = ?;

  bit IsTruncStore = ?;

  bit IsAtomicOrderingMonotonic = ?;

  bit IsAtomicOrderingAcquire = ?;

  bit IsAtomicOrderingRelease = ?;

  bit IsAtomicOrderingAcquireRelease = ?;

  bit IsAtomicOrderingSequentiallyConsistent = ?;

  bit IsAtomicOrderingAcquireOrStronger = ?;

  bit IsAtomicOrderingReleaseOrStronger = ?;

  ValueType MemoryVT = ?;

  ValueType ScalarMemoryVT = ?;

}

2.2.4.2.2. 其他特殊PatFrag派生类

这包括以下的这些定义,它们都是PatFrag的派生类,可视为PatFrag的偏特化(Target.td,v7.0挪到TargetSelectionDAG.td)。

618     class OutPatFrag<dag ops, dag frag>

619     : PatFrag<ops, frag, [{}], NOOP_SDNodeXForm>;

 

623     class PatLeaf<dag frag, code pred = [{}], SDNodeXForm xform = NOOP_SDNodeXForm>

624     : PatFrag<(ops), frag, pred, xform>;

 

640     class ImmLeaf<ValueType vt, code pred, SDNodeXForm xform = NOOP_SDNodeXForm>

641       : PatFrag<(ops), (vt imm), [{}], xform> {

642       let ImmediateCode = pred;

643       bit FastIselShouldIgnore = 0;

644     }

OutPatFrag适用于输出模板,不需要处理操作数的代码片段。而PatLeaf则是没有操作数,可以简洁地定义立即数等通用结构。ImmLeaf则视为引入限定的PatLeaf,专用于立即数。

V7.0扩展了ImmLeaf的定义(增加两个域IsAPIntIsAPFloat),并在此基础上派生了IntImmLeafFPImmLeaf,分别用于表示整形与浮点型的立即数。同时定义了整形与浮点向量类型的PatLeaf派生定义,以及全1及全0的向量立即数。

2.2.5. 另一种匹配方式

另一种匹配指令的方法是通过Pattern(Target.td,v7.0挪到TargetSelectionDAG.td)。它是这样的一个定义:

1067  class Pattern<dag patternToMatch, list<dag> resultInstrs> {

1068    dag PatternToMatch = patternToMatch;

1069    list<dag> ResultInstrs  = resultInstrs;

1070    list<Predicate> Predicates = [];  // See class Instruction in Target.td.

1071    int AddedComplexity = 0;   // See class Instruction in Target.td.

1072  }

对于一些CPU,尤其CISC系统,一些比较复杂的操作,比如乘加、移位加,既可以通过一组指令完成,也存在一条可以完成这个操作的比较复杂的指令。通常比较复杂的指令要更高效一些,而且通常产生的执行代码更小。因此在这种情形下,编译器最好能选择复杂的指令。Pattern就有助于解决这个问题。参数patternToMatch给出这个复杂操作的DAG模式,resultInstrs则列出匹配指令的匹配模式,LLVM将使用贪婪匹配自动完成这个指令选择。

因为通常只会输出一条指令,还因此定义了Pat作为一个方便的记法。

1076  class Pat<dag pattern, dag result> : Pattern<pattern, [result]>;

2.2.5.1. 谓词Predicate

在Pattern与Instruction的定义中都有谓词部分(Predicates),只有满足谓词设定的条件,Pattern与Instruction的匹配才能继续。Predicate定义在Target.td文件里:

477     class Predicate<string cond> {

478       string CondString = cond;

479    

480       /// AssemblerMatcherPredicate - If this feature can be used by the assembler

481       /// matcher, this is true.  Targets should set this by inheriting their

482       /// feature from the AssemblerPredicate class in addition to Predicate.

483       bit AssemblerMatcherPredicate = 0;

484    

485       /// AssemblerCondString - Name of the subtarget feature being tested used

486       /// as alternative condition string used for assembler matcher.

487       /// e.g. "ModeThumb" is translated to "(Bits & ModeThumb) != 0".

488       ///      "!ModeThumb" is translated to "(Bits & ModeThumb) == 0".

489       /// It can also list multiple features separated by ",".

490       /// e.g. "ModeThumb,FeatureThumb2" is translated to

491       ///      "(Bits & ModeThumb) != 0 && (Bits & FeatureThumb2) != 0".

492       string AssemblerCondString = "";

493    

494       /// PredicateName - User-level name to use for the predicate. Mainly for use

495       /// in diagnostics such as missing feature errors in the asm matcher.

496       string PredicateName = "";

497     }

V7.0增加了域:bit RecomputePerFunction = 0;表示每次函数改变都要重新计算谓词。

Predicate实际上封装了一段嵌入代码。下面是一个具体的谓词定义(X86InstrInfo.td):

787     def HasBMI: Predicate<"Subtarget->hasBMI()">;

这个谓词测试目标机器是否支持BMI指令(位操作指令,Bit Manipulate Instruction),这是在Haswell微架构里引入的,分BMI1与BMI2两个指令集。这里是BMI1,下面这个是测试BMI2:

788     def HasBMI2: Predicate<"Subtarget->hasBMI2()">;

很显然,Predicate定义中的CondString是一个C++代码片段,TableGen将会把它插入到生成代码中合适的地方。

谓词主要是用于筛选处理器所支持的指令集。另外,也可以对该处理器架构的某些有局限的指令进行筛选,比如某个处理器的比特设置指令比较慢,应该用别的指令替代(不过在LLVM,目前后者还只是设想)。这些谓词的来源与声明我们在指令调度的时候再来看。

2.2.5.2. 指令定义的例子

下面是使用谓词HasBMI的指令定义的例子(X86InstrArithmetic.td,v7.0对这些定义都增加了设置AddedComplexity = -6):

1280  let Predicates = [HasBMI] in {

1281    def : Pat<(and (not GR32:$src1), GR32:$src2),

1282              (ANDN32rr GR32:$src1, GR32:$src2)>;

1283    def : Pat<(and (not GR64:$src1), GR64:$src2),

1284              (ANDN64rr GR64:$src1, GR64:$src2)>;

1285    def : Pat<(and (not GR32:$src1), (loadi32 addr:$src2)),

1286              (ANDN32rm GR32:$src1, addr:$src2)>;

1287    def : Pat<(and (not GR64:$src1), (loadi64 addr:$src2)),

1288              (ANDN64rm GR64:$src1, addr:$src2)>;

1289  }

匹配的模式并不复杂,其中GR32/GR64也是TableGen的特殊对象,专门表示32/64位通用寄存器。而在作为ResultInstrs的部分,ANDNXX是通过下面结构来定义的(X86InstrArithmetic.td,v7.0由于x86不再使用InstrItinClass,下面的IIC_BIN_NONMEMIIC_BIN_MEM都去掉了。另外,1275行的对应设置增加了AddedComplexity = -6):

1262  multiclass bmi_andn<string mnemonic, RegisterClass RC, X86MemOperand x86memop,

1263                      PatFrag ld_frag> {

1264    def rr : I<0xF2, MRMSrcReg, (outs RC:$dst), (ins RC:$src1, RC:$src2),

1265              !strconcat(mnemonic, "\t{$src2, $src1, $dst|$dst, $src1, $src2}"),

1266              [(set RC:$dst, EFLAGS, (X86and_flag (not RC:$src1), RC:$src2))],

1267              IIC_BIN_NONMEM>, Sched<[WriteALU]>;

1268    def rm : I<0xF2, MRMSrcMem, (outs RC:$dst), (ins RC:$src1, x86memop:$src2),

1269              !strconcat(mnemonic, "\t{$src2, $src1, $dst|$dst, $src1, $src2}"),

1270              [(set RC:$dst, EFLAGS,

1271               (X86and_flag (not RC:$src1), (ld_frag addr:$src2)))], IIC_BIN_MEM>,

1272             Sched<[WriteALULd, ReadAfterLd]>;

1273  }

1274 

1275  let Predicates = [HasBMI], Defs = [EFLAGS] in {

1276    defm ANDN32 : bmi_andn<"andn{l}", GR32, i32mem, loadi32>, T8PS, VEX_4V;

1277    defm ANDN64 : bmi_andn<"andn{q}", GR64, i64mem, loadi64>, T8PS, VEX_4V, VEX_W;

1278  }

Multiclass与defm用于一次定义多个类及定义。在defm处,产生的def的名字将是该defm名字与对应multiclass中def名字的结合,例如ANDN32将产生ANDN32rr与ANDN32rm。TableGen支持几个类似C的内置函数,比如上面的strconcat,要使用这些函数必须以“!”开头。

以1281行的定义为例,这个例子匹配一个and (not GR32:$src1), GR32:$src2)”模式的IR dag匹配为一条ANDN32rr指令。实际上这个定义将ISD::AND选中到X86ISD::AND(在支持BMI指令的情形下),而两者要匹配的操作数是一样的(因此如果谓词成立的话,选中ISD::AND,也必然选中X86ISD::AND)。

这其中的区别是,ISD::AND是前端从C/C++程序得到的LLVM IR形式的DAG中的一部分,它不关心受影响的标记,而且也是目标机器无关的。X86ISD::AND则可能会影响标记(或受标记影响),而且可能是特定目标机器的优化指令(比如这里的BMI指令,如果不支持BMI指令,就需要两条指令来完成ANDN32rr的操作)。

事实上,2.1.4节中给出的IMUL16rri例子,在X86InstrCompiler.td也有一个对应的匿名Pat定义,将ISD::MUL匹配作X86ISD::MUL。

另外,这个例子还包含了一些有趣的深层约束。在这个例子里它使用的操作数的类型是X86MemOperand,它有这样的定义(X86InstrInfo.td,v7.0299行最后参数改为SEGMENT_REG):

296     class X86MemOperand<string printMethod,

297               AsmOperandClass parserMatchClass = X86MemAsmOperand> : Operand<iPTR> {

298       let PrintMethod = printMethod;

299       let MIOperandInfo = (ops ptr_rc, i8imm, ptr_rc_nosp, i32imm, i8imm);

300       let ParserMatchClass = parserMatchClass;

301       let OperandType = "OPERAND_MEMORY";

302     }

这样的操作数有时需要比较复杂的计算,比如上面的X86取址。这需要描述其中涉及的的子操作数,由299行的MIOperandInfo保存它们。注意,这是一个dag值,操作符必须是ops。

与这个操作数对应,在相关Instruction定义的Pattern域里,addr是这样一个ComplexPattern定义(X86InstrInfo.td):

701     def addr: ComplexPattern<iPTR, 5, "SelectAddr", [], [SDNPWantParent]>;

在这个定义中,numops是5,正好是子操作数的数目,而这些子操作数最终会传递给addr中指定的方法SelectAddr。

上面的所有这些定义TableGen首先使用TGLexer与TGParser进行词法、语法解析,所有的class与def定义都会生成一个对应的Record实例。而所有的值则会创建为Init的派生实例,这还包括A.B,A[B]这样的表达式。所有的Record实例都保存在一个RecordKeeper类型的容器里,在这个容器里,class与def的定义是分开放置的,因此它们彼此间可以同名。

实际上,TD文件中所有的值都会被创建为Init的派生实例。在LLVM源代码目录/include/llvm/ TableGen/Record.h中给出了描述Init派生类型的枚举类型:

235       enum InitKind {

236         IK_BitInit,  // true/false

237         IK_FirstTypedInit,

238         IK_BitsInit,   // { a, b, c }

239         IK_DagInit,  // Dag

240         IK_DefInit,   // 在描述里援引的一个def

241         IK_FieldInit,   // X.Y

242         IK_IntInit,

243         IK_ListInit,    // [AL, AH, CL]

244         IK_FirstOpInit,

245         IK_BinOpInit,

246         IK_TernOpInit,

247         IK_UnOpInit,

248         IK_LastOpInit,

249         IK_StringInit,

250         IK_VarInit,    // 对整个变量对象的引用

251         IK_VarListElementInit,  // List[4]

252         IK_LastTypedInit,

253         IK_UnsetInit,  // 未初始化

254         IK_VarBitInit  // 对变量或域的一个比特的访问

255       };

注意237与252行,在这中间的是所谓的“有类型的Init”,它们实际上是继承自TypedInit。另外,244与248行的IK_FirstOpInit与IK_LastOpInit也不代表具体的Init派生类型,而是表示它们之间的是所谓的“代表操作数的Init”(它们实际继承自OpInit,OpInit又继承自TypedInit)。其中IK_BinOpInit代表二元操作符的操作数,IK_TernOpInit则代表三元操作符(TableGen支持IF,Foreach,Subst这3个三元操作符)的操作符,IK_UnOpInit则是一元操作符的操作数。其余参见注释说明。

2.2.5.3. 指令展开的例子

对于这个Pat定义:

1281    def : Pat<(and (not GR32:$src1), GR32:$src2),

1282              (ANDN32rr GR32:$src1, GR32:$src2)>;

展开后的结果是:

def anonymous_2021 {  // Pattern Pat

  dag PatternToMatch = (and (not GR32:$src1), GR32:$src2);

  list<dag> ResultInstrs = [(ANDN32rr GR32:$src1, GR32:$src2)];

  list<Predicate> Predicates = [HasBMI];

  int AddedComplexity = 0;

  string NAME = ?;

}

V7.0的结果则是:

def anonymous_1038 {    // Pattern Pat

  dag PatternToMatch = (and (not GR32:$src1), GR32:$src2);

  list<dag> ResultInstrs = [(ANDN32rr GR32:$src1, GR32:$src2)];

  list<Predicate> Predicates = [HasBMI];

  int AddedComplexity = -6;

}

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值