LLVM学习笔记(8)

2.3. 汇编处理描述

至于关于读写汇编格式指令信息的封装,TableGen提供了类Target(target.td)作为各目标机器的基类。

1059  class Target {

1060    // InstructionSet - Instruction set description for this target.

1061    InstrInfo InstructionSet;

1062 

1063    // AssemblyParsers - The AsmParser instances available for this target.

1064    list AssemblyParsers = [DefaultAsmParser];

1065 

1066    /// AssemblyParserVariants - The AsmParserVariant instances available for

1067    /// this target.

1068    list AssemblyParserVariants = [DefaultAsmParserVariant];

1069 

1070    // AssemblyWriters - The AsmWriter instances available for this target.

1071    list AssemblyWriters = [DefaultAsmWriter];

              <-- v7.0增加

// AllowRegisterRenaming - Controls whether this target allows

// post-register-allocation renaming of registers.  This is done by

// setting hasExtraDefRegAllocReq and hasExtraSrcRegAllocReq to 1

// for all opcodes if this flag is set to 0.

int AllowRegisterRenaming = 0;

1072  }

AssemblyParsers与AssemblyParserVariants是目标机器相关的汇编代码解析描述,如果子目标机器需要使用自己的汇编解析描述,这些解析器描述由AssemblyParserVariants保存。Target定义中最重要的是InstrInfo,它代表输出汇编所关注的目标机器指令信息:

690     class InstrInfo {

691       // Target can specify its instructions in either big or little-endian formats.

692       // For instance, while both Sparc and PowerPC are big-endian platforms, the

693       // Sparc manual specifies its instructions in the format [31..0] (big), while

694       // PowerPC specifies them using the format [0..31] (little).

695       bit isLittleEndianEncoding = 0;

696    

697       // The instruction properties mayLoad, mayStore, and hasSideEffects are unset

698       // by default, and TableGen will infer their value from the instruction

699       // pattern when possible.

700       //

701       // Normally, TableGen will issue an error it it can't infer the value of a

702       // property that hasn't been set explicitly. When guessInstructionProperties

703       // is set, it will guess a safe value instead.

704       //

705       // This option is a temporary migration help. It will go away.

706       bit guessInstructionProperties = 1;

707    

708       // TableGen's instruction encoder generator has support for matching operands

709       // to bit-field variables both by name and by position. While matching by

710       // name is preferred, this is currently not possible for complex operands,

711       // and some targets still reply on the positional encoding rules. When

712       // generating a decoder for such targets, the positional encoding rules must

713       // be used by the decoder generator as well.

714       //

715       // This option is temporary; it will go away once the TableGen decoder

716       // generator has better support for complex operands and targets have

717      // migrated away from using positionally encoded operands.

718       bit decodePositionallyEncodedOperands = 0;

719    

720       // When set, this indicates that there will be no overlap between those

721       // operands that are matched by ordering (positional operands) and those

722       // matched by name.

723       //

724       // This option is temporary; it will go away once the TableGen decoder

725       // generator has better support for complex operands and targets have

726      // migrated away from using positionally encoded operands.

727       bit noNamedPositionallyEncodedOperands = 0;

728     }

X86目标机器原封不动地从InstrInfo派生了X86InstrInfo。它的Target派生定义则是:

568     def X86 : Target {

569       // Information about the instructions...

570       let InstructionSet = X86InstrInfo;

571       let AssemblyParsers = [ATTAsmParser];    <-- v7.0删除

572       let AssemblyParserVariants = [ATTAsmParserVariant, IntelAsmParserVariant];

573       let AssemblyWriters = [ATTAsmWriter, IntelAsmWriter];

             let AllowRegisterRenaming = 1;      <--v7.0增加

574     }

正如我们所熟悉的X86的汇编代码分为了AT&T与Intel格式。ATTAsmParserVariant这样的定义给出了对应汇编语言的注释符,分隔符以及寄存器前缀等的定义,而ATTAsmWriter这样的定义则指出LLVM中的哪个类可以输出指定的汇编格式代码。ATTAsmParser则指出LLVM需要使用AsmParser来住持汇编解析(v7.0使用DefaultAsmParser)。

2.4. 目标机器描述

对目标机器处理器的描述则以Target.td文件中的Processor定义为基类。

1119  class Processor f> {

1120    // Name - Chip set name.  Used by command line (-mcpu=) to determine the

1121    // appropriate target chip.

1122    //

1123    string Name = n;

1124 

1125    // SchedModel - The machine model for scheduling and instruction cost.

1126    //

1127    SchedMachineModel SchedModel = NoSchedModel;

1128 

1129    // ProcItin - The scheduling information for the target processor.

1130    //

1131    ProcessorItineraries ProcItin = pi;

1132 

1133    // Features - list of

1134    list<SubtargetFeature> Features = f;

1135  }

1127行的SchedModel与1131行的ProcItin都可以对调度细节进行描述。SchedModel具体的描述参见下面的定义,ProcItin则是描述各种指令的执行步骤。1127行的NoSchedModel使得Processor定义缺省不使用SchedModel。不过,对于某些处理器,ProcItin并不方便使用,而是使用SchedModel,因此也有缺省禁止ProcItin的Processor定义:

1143  class ProcessorModel f>

1144    : Processor {

1145    let SchedModel = m;

1146  }

下面我们将看到对于Atom这样的顺序流水线机器,使用的是ProcItin,而Sandy Bridge这样支持乱序执行的机器,则使用SchedModel。我们在下面讨论这些细节。

​​​​​​​2.4.1. 特性描述

1134行的Features用于描述处理器所支持的特性,比如是否支持SSE指令集,是否支持TSX指令集等。TableGen将根据这些描述生成需要的谓词判断,SubtargetFeature是一个全字符串的定义:

1077  class SubtargetFeature

1078                         list i = []> {

1079    // Name - Feature name.  Used by command line (-mattr=) to determine the

1080    // appropriate target chip.

1081    //

1082    string Name = n;

1083 

1084    // Attribute - Attribute to be set by feature.

1085    //

1086    string Attribute = a;

1087 

1088    // Value - Value the attribute to be set to by feature.

1089    //

1090    string Value = v;

1091 

1092    // Desc - Feature description.  Used by command line (-mattr=) to display help

1093    // information.

1094    //

1095    string Desc = d;

1096 

1097    // Implies - Features that this feature implies are present. If one of those

1098    // features isn't set, then this one shouldn't be set either.

1099    //

1100    list Implies = i;

1101  }

1100行的Implies表示,如果具有该特性,该特性隐含包含了哪些特性。

SubTargetFeature具有很好的灵活性以及描述能力,能够描述差异很大的处理器。以Atom处理器为例,这是Intel推出的面向移动设备的处理器。它与传统的Intel处理器相比,更像RISC处理器。TableGen这样定义它:

240     // Atom CPUs.

241     class BonnellProc : ProcessorModelAtomModel, [

242                                       ProcIntelAtom,

243                                        FeatureSSSE3,

244                                        FeatureCMPXCHG16B,

245                                        FeatureMOVBE,

246                                        FeatureSlowBTMem,     <-- v7.0删除

247                                        FeatureLeaForSP,

248                                        FeatureSlowDivide32,

249                                        FeatureSlowDivide64,

250                                        FeatureCallRegIndirect,   <-- v7.0删除

251                                        FeatureLEAUsesAG,

252                                        FeaturePadShortFunctions

                                               FeatureX87,     <-- v7.0增加

                                               FeatureMMX,

                                               FeatureFXSR,

                                               FeatureNOPL,

                                               FeatureSlowUAMem16,

                                               FeatureSlowTwoMemOps,

                                               FeatureLAHFSAHF

253                                      ]>;

254     def : BonnellProc<"bonnell">;

255     def : BonnellProc<"atom">; // Pin the generic name to the baseline.

242~252行就是Atom处理器支持的特性。第一个ProcIntelAtom是对这个处理器族的文字描述,帮助开发人员理解。而接着的FeatureSSSE3表示Atom支持SSSE3指令集,这同时也意味着也支持SSSE3以下的指令集,即SSE3、SSE2、SSE。

54       def FeatureSSSE3: SubtargetFeature<"ssse3", "X86SSELevel", "SSSE3",

55                                             "Enable SSSE3 instructions",

56                                             [FeatureSSE3]>;

这个定义表示只支持SSSE3及以下的指令集(因此还包括SSE3,SSE2,SSE,MMX等)。

75       def FeatureCMPXCHG16B: SubtargetFeature<"cx16", "HasCmpxchg16b", "true",

76                                             "64-bit with cmpxchg16b",

77                                             [Feature64Bit]>;

这个定义表示支持64位cmpxchg16b指令及条件move指令。

143     def FeatureMOVBE: SubtargetFeature<"movbe", "HasMOVBE", "true",

144                                           "Support MOVBE instruction">;

这个定义表示支持MOVBE指令。

78       def FeatureSlowBTMem: SubtargetFeature<"slow-bt-mem", "IsBTMemSlow", "true",

79                                              "Bit testing of memory is slow">;

这个定义表示该处理器上的比特测试指令是慢的。

173     def FeatureLeaForSP: SubtargetFeature<"lea-sp", "UseLeaForSP", "true",

174                                          "Use LEA for adjusting the stack pointer">;

这个定义表示使用LEA指令来调整栈指针。

175     def FeatureSlowDivide32: SubtargetFeature<"idivl-to-divb",

176                                          "HasSlowDivide32", "true",

177                                          "Use 8-bit divide for positive values less than 256">;

这个定义表示对小于256的正数使用8位除法(因为32位除法慢)。

178     def FeatureSlowDivide64: SubtargetFeature<"idivq-to-divw",

179                                          "HasSlowDivide64", "true",

180                                          "Use 16-bit divide for positive values less than 65536">;

这个定义表示对小于65535的正数使用16位除法(因为64位除法慢)。

184     def FeatureCallRegIndirect: SubtargetFeature<"call-reg-indirect",

185                                          "CallRegIndirect", "true",

186                                          "Call register indirect">;

这个定义表示间接调用寄存器。

187     def FeatureLEAUsesAG: SubtargetFeature<"lea-uses-ag", "LEAUsesAG", "true",

188                                        "LEA instruction needs inputs at AG stage">;

这个定义表示LEA指令在AG阶段(访问数据缓存的某个阶段)就需要输入。

181     def FeaturePadShortFunctions: SubtargetFeature<"pad-short-functions",

182                                          "PadShortFunctions", "true",

183                                          "Pad short functions">;

这个定义表示要填充短的函数。

这些SubtargetFeature的定义由一个专门的TableGen选项“-gen-subtarget”来解析,在构建编译器时,将通过llvm-tblgen执行该选项来生成一个目标机器特定的文件X86GenSubtargetInfo.inc(以X86为例)。这个文件包含的代码将辅助构建一个X86Subtarget类的实例(X86Subtarget.h),这个实例将提供相应的判断方法。

​​​​​​​2.4.2. 调度信息

SchedMachineModel是如下描述乱序执行处理器的。在TableGen解析后,生成MCSchedModel实例。V7.0 MCSchedModel的注释是这样说的(MCSchedule.h):

这个机器模型直接向调度器,以属性的形式,提供了关于微架构的基本信息。它还可选地援引调度器资源表以及行程表(Itinerary table)。调度器资源表对每个指令类型模拟时延与代价。行程表是一个无关的机制,提供了一个描述每周期指令执行的细致保留表。依赖于CPU类型以及选中的调度器,子目标(subtarget)可以定义上述任意或所有的数据类别。

这里定义的机器无关属性被调度器用作抽象的机器模型。一个真实的微架构有若干缓冲、队列以及阶段(stage)。不能声明一个给定的机器无关抽象属性对应一个跨越所有子目标的特定物理属性。不过,这个抽象模型是有用的。另外,子目标通常使用处理器特定资源,扩展这个模型来模拟任何可被调度启发式利用以及在抽象模型里描述不充分的硬件特性。

抽象流水线是围绕“发布点(issue point)”的概念构建的。这仅是机器周期计数的一个参考点。物理机器将具有延迟执行的流水线阶段。调度器没有模拟这些延迟,因为它们是不相关的,只要它们是一致的。除了固有时延,在指令彼此间有不同的执行时延时会引起误差。这些特殊情形可由TableGen构造处理,比如ReadAdvance,它在读数据时降低时延,以及ResourceCycles,它在为若干抽象周期写数据时,消耗一个处理器资源。

TODO:当前缺少的一个工具是向ResourceCycles添加时延的能力。这应该容易添加且很可能覆盖所有当前由行程表处理的情形。

关于乱序执行,以及更一般的,指令缓冲的说明。CPU流水线的部分总是顺序的。发布点,它是周期计数的参考点,仅在作为流水线的顺序部分时是合理的。流水线的其他部分有时落后,有时跟上。如果流水线的其他解耦部分的资源限制可以调度器可资利用的方式预测,模拟它们才是令人感兴趣的。

LLVM机器模型区分顺序约束与乱序约束,因此目标机器的调度策略可以采用合适的启发式。对良好平衡的CPU流水线,乱序资源通常不会被处理为一个困难的调度约束。例如,在GenericScheduler中,由有限乱序资源导致的时延不会直接反映在调度器在发布一条指令与其依赖指令间看到的周期数中。换而言之,乱序资源不直接增加指令对间的时延。不过,它们仍然被用于检测跨越指令序列的潜在瓶颈,并适当地偏移调度启发式。

77       class SchedMachineModel {

78         int IssueWidth = -1; // Max micro-ops that may be scheduled per cycle.

79         int MinLatency = -1; // Determines which instructions are allowed in a group.  <-- v7.0删除

80                              // (-1) inorder (0) ooo, (1): inorder +var latencies.

81         int MicroOpBufferSize = -1; // Max micro-ops that can be buffered.

82         int LoopMicroOpBufferSize = -1; // Max micro-ops that can be buffered for

83                                         // optimized loop dispatch/execution.

84         int LoadLatency = -1; // Cycles for loads to access the cache.

85         int HighLatency = -1; // Approximation of cycles for "high latency" ops.

86         int MispredictPenalty = -1; // Extra cycles for a mispredicted branch.

87      

88         // Per-cycle resources tables.

89         ProcessorItineraries Itineraries = NoItineraries;

90      

91         bit PostRAScheduler = 0; // Enable Post RegAlloc Scheduler pass.

92      

93         // Subtargets that define a model for only a subset of instructions

94         // that have a scheduling class (itinerary class or SchedRW list)

95         // and may actually be generated for that subtarget must clear this

96         // bit. Otherwise, the scheduler considers an unmodelled opcode to

97         // be an error. This should only be set during initial bringup,

98         // or there will be no way to catch simple errors in the model

99         // resulting from changes to the instruction definitions.

100       bit CompleteModel = 1;

101    

102       bit NoModel = 0; // Special tag to indicate missing machine model.

   <--  v7.0增加

// Indicates that we should do full overlap checking for multiple InstrRWs

// definining the same instructions within the same SchedMachineModel.

// FIXME: Remove when all in tree targets are clean with the full check

// enabled.

bit FullInstRWOverlapCheck = 1;

// A processor may only implement part of published ISA, due to either new ISA

// extensions, (e.g. Pentium 4 doesn't have AVX) or implementation

// (ARM/MIPS/PowerPC/SPARC soft float cores).

//

// For a processor which doesn't support some feature(s), the schedule model

// can use:

//

// let UnsupportedFeatures = [HaveA,..,HaveY];

//

// to skip the checks for scheduling information when building LLVM for

// instructions which have any of the listed predicates in their Predicates

// field.

list UnsupportedFeatures = [];

103     }

属性的缺省值定义在C++类MCSchedModel(MCSchedule.h)中。在SchedMachineModel中出现的值-1表示该目标机器不会改写该属性。

其中IssueWidth缺省为1,它是一个周期内可以调度(发布)的最大指令数。这意味着一个严格的顺序约束(即“赌运气(hazard)”)。在GenericScheduler策略中,在一个特定周期中不能调度超过IssueWidth个微操作。

在实践中,IssueWidth对模拟解码器(微操作展开后)以及乱序保留站间的瓶颈或者解码器带宽本身是有用的。如果保留站总数也是瓶颈,或者其他流水线阶段有带宽限制,可以通过添加一个乱序处理器资源自然地模拟。

MicroOpBufferSize缺省是0,它是处理器为乱序执行所缓冲的微操作个数。0表示在本周期未就绪的操作不考虑调度(它们进入挂起队列)。指令时延是最重要的。如果在一次调度中挂起许多指令,可能会更高效。1表示不管在本周期是否就绪,考虑调度所有的指令。指令时延仍然会导致发布暂停,不过我们通过其他启发式平衡这些暂停。>1表示处理器乱序执行。这是高度特定于比如寄存器重命名池及重排缓冲,这些机器特性的一个机器无关的估计。

LoopMicroOpBufferSize的缺省值是0。它是处理器为了优化循环的执行可能缓冲的微操作个数。更一般地,这代表了一个循环体里最优的微操作数。循环可能被部分展开使得循环体的微操作数更接近这个值。

LoadLatency的缺省值是4。它是load指令的预期时延。如果MinLatency >= 0,对个别读操作,可以通过InstrItinerary的OperandCycles来覆盖它(v7.0没有MinLatency)。

HighLatency的缺省值是10。它是“非常高时延”操作的预期时延。通常,这是可能对调度启发式产生一些影响的一个任意高的周期数。如果MinLatency >= 0,可以通过InstrItinerary的OperandCycles来覆盖它(v7.0没有MinLatency)。

MispredictPenalty的缺省值是10。它是处理器从一次跳转误判恢复所需的典型额外周期数。

NoModel如果是1,表示这个SchedMachineModel定义是没有实际意义的,比如:

105     def NoSchedModel : SchedMachineModel {

106       let NoModel = 1;

107     }

看到在SchedMachineModel的定义里也包含了一个ProcessorItineraries类型的成员,注释中提到这是每周期的资源表(缺省是不提供)。同时ProcessorItineraries也是Processor中ProcItin的类型,它的定义是(TargetItinerary.td):

126     class ProcessorItineraries fu, list bp,

127                                list iid> {

128       list FU = fu;

129       list BP = bp;

130       list IID = iid;

131     }

其成员都是list列表,列表的每个元素对应一个处理器周期。FuncUnit描述的是处理器的组成单元。因为处理器单元形形色色、各式各样,因此FuncUnit作为基类只是个空的class。类似的,Bypass用于描述流水线旁路,它也是一个空class。

下面我们看一些例子。

2.4.2.1. ATOM的描述

Atom是Intel设计的超低电压IA-32与x86-64微处理器,它基于Bonnell微架构。因此在X86.td的255行,可以看到Atom的ProcessorModel定义正是从BonnellProc派生的。

Bonnell微架构每周期可以最多执行两条指令。像许多其他x86微处理器,在执行前它把x86指令(CISC指令)翻译为更简单的内部操作(有时称为微操作,即实质上RISC形式的指令)。在翻译时,在典型的程序里,大多数指令产生一个微操作,大约4%的指令产生多个微操作。生成多个微操作的指令数显著少于P6及NetBurst微架构。在Bonnell微架构里,内部的微操作可以同时包含与一个ALU操作关联的一个内存读及一个内存写,因此更类似于x86水平,比之前设计中使用的微操作更强大。这使得仅使用两个整数ALU,无需指令重排、推测执行或寄存器重命名,就获得相对好的性能。因此Bonnell微架构代表用在Intel更早期设计的原则,比如P5与i486,的一个部分复兴,唯一的目的是提高每瓦特性能比。不过,超线程以一个简单的方式(即低功耗)实现,通过避免典型的简单线程依赖来提高流水线效率。

要描述这个处理器,首先在X86.td文件里可以看到这些定义(这只是X86处理器定义集中的很小一部分):

203     def ProcIntelAtom: SubtargetFeature<"atom", "X86ProcFamily", "IntelAtom",

204                         "Intel Atom processors">;

205     def ProcIntelSLM: SubtargetFeature<"slm", "X86ProcFamily", "IntelSLM",

206                         "Intel Silvermont processors">;

207    

208     class Proc Features>

209     : ProcessorModelGenericModel, Features>;

210    

211     def : Proc<"generic",         []>;

212     def : Proc<"i386",            []>;

213     def : Proc<"i486",            []>;

214     def : Proc<"i586",            []>;

215     def : Proc<"pentium",         []>;

216     def : Proc<"pentium-mmx",     [FeatureMMX]>;

217     def : Proc<"i686",            []>;

218     def : Proc<"pentiumpro",      [FeatureCMOV]>;

219     def : Proc<"pentium2",        [FeatureMMX, FeatureCMOV]>;

220     def : Proc<"pentium3",        [FeatureSSE1]>;

221     def : Proc<"pentium3m",       [FeatureSSE1, FeatureSlowBTMem]>;

222     def : Proc<"pentium-m",       [FeatureSSE2, FeatureSlowBTMem]>;

223     def : Proc<"pentium4",        [FeatureSSE2]>;

224     def : Proc<"pentium4m",       [FeatureSSE2, FeatureSlowBTMem]>;

225    

226     // Intel Core Duo.

227     def : ProcessorModel<"yonah", SandyBridgeModel,

228                          [FeatureSSE3, FeatureSlowBTMem]>;

229    

230     // NetBurst.

231     def : Proc<"prescott", [FeatureSSE3, FeatureSlowBTMem]>;

232     def : Proc<"nocona",   [FeatureSSE3, FeatureCMPXCHG16B, FeatureSlowBTMem]>;

233    

234     // Intel Core 2 Solo/Duo.

235     def : ProcessorModel<"core2", SandyBridgeModel,

236                          [FeatureSSSE3, FeatureCMPXCHG16B, FeatureSlowBTMem]>;

237     def : ProcessorModel<"penryn", SandyBridgeModel,

238                          [FeatureSSE41, FeatureCMPXCHG16B, FeatureSlowBTMem]>;

239    

240     // Atom CPUs.

241     class BonnellProc : ProcessorModelAtomModel, [

242                                        ProcIntelAtom,

243                                        FeatureSSSE3,

244                                        FeatureCMPXCHG16B,

245                                        FeatureMOVBE,

246                                        FeatureSlowBTMem,     <-- v7.0删除

247                                        FeatureLeaForSP,

248                                        FeatureSlowDivide32,

249                                        FeatureSlowDivide64,

250                                        FeatureCallRegIndirect,   <-- v7.0删除

251                                        FeatureLEAUsesAG,

252                                        FeaturePadShortFunctions

                                               FeatureX87,     <-- v7.0增加

                                               FeatureMMX,

                                               FeatureFXSR,

                                               FeatureNOPL,

                                               FeatureSlowUAMem16,

                                               FeatureSlowTwoMemOps,

                                               FeatureLAHFSAHF

253                                      ]>;

254     def : BonnellProc<"bonnell">;

255     def : BonnellProc<"atom">; // Pin the generic name to the baseline.

256    

257     class SilvermontProc : ProcessorModel

258                                           ProcIntelSLM,

259                                           FeatureSSE42,

260                                           FeatureCMPXCHG16B,

261                                           FeatureMOVBE,

262                                           FeaturePOPCNT,

263                                           FeaturePCLMUL,

264                                           FeatureAES,

265                                           FeatureSlowDivide64,

266                                           FeatureCallRegIndirect,   <-- v7.0删除

267                                           FeaturePRFCHW,

268                                           FeatureSlowLEA,

269                                           FeatureSlowIncDec,

270                                           FeatureSlowBTMem,        <-- v7.0删除

271                                           FeatureFastUAMem         ß<-- v7.0删除

                                                 FeatureX87,     <-- v7.0增加

                                                 FeatureMMX,

                                                 FeatureFXSR,

                                                 FeatureNOPL,

                                                 FeatureSlowTwoMemOps,

                                                 FeatureLAHFSAHF,

                                                 FeaturePOPCNTFalseDeps,

                                                 FeatureRDRAND,

                                                 FeatureSlowPMULLD

272                                         ]>;

273     def : SilvermontProc<"silvermont">;

274     def : SilvermontProc<"slm">; // Legacy alias.

首先203与205行定义了两个SubtargetFeature的派生定义,分别用于表示Atom与Silvermont型号处理器的特性。208行Proc定义使用GenericModel作为描述模型。这个处理器模型用于粗略描述处理器(比如没有提供处理器完整的指令执行细节文档的情形)。

637     def GenericModel: SchedMachineModel {

638       let IssueWidth = 4;

639       let MicroOpBufferSize = 32;

640       let LoadLatency = 4;

641       let HighLatency = 10;

642       let PostRAScheduler = 0;

643     }

GenericModel不包含每周期的资源表。IssueWidth类似于解码单元的数量。Core与其子代,包括Nehalem及SandyBridge有4个解码器。解码器之外的资源执行微操作并被缓冲,因此相邻的微操作不会直接竞争。

MicroOpBufferSize > 1表示未经处理的依赖性可以在同一个周期里解码。对运行中的指令,值32是一个合理的主观值。

HighLatency = 10是乐观的。X86InstrInfo::isHighLatencyDef标志高时延的操作码。或者,在这里包含InstrItinData项来定义特定的操作数时延。因为这些时延不用于流水线冲突(pipeline hazard),它们不需要精确。

这里可以看到,即使GenericModel也是对Core及之后的处理器优化的,这个设置对老旧的Intel处理器并不适合(它们没有这么多解码单元,也没有微操作缓冲)。不过对指令调度来说,没有什么太大问题,只是会使老旧处理器的效率不高而已。

v7.0上述定义为(没有太大的差异):

class GenericX86Model : SchedMachineModel {

  let IssueWidth = 4;

  let MicroOpBufferSize = 32;

  let LoadLatency = 4;

  let HighLatency = 10;

  let PostRAScheduler = 0;

  let CompleteModel = 0;

}

 

def GenericModel : GenericX86Model;

上面的处理器定义用到了许多特性描述,其中FeatureMMX表示处理器支持符合MMX标准的指令与寄存器:

41       def FeatureMMX: SubtargetFeature<"mmx","X86SSELevel", "MMX",

42                                             "Enable MMX instructions">;

而Atom的SchedMachineModel派生定义是这样的:

537     def AtomModel : SchedMachineModel {

538       let IssueWidth = 2;  // Allows 2 instructions per scheduling group.

539       let MicroOpBufferSize = 0; // In-order execution, always hide latency.

540       let LoadLatency = 3; // Expected cycles, may be overriden by OperandCycles.

541       let HighLatency = 30;// Expected, may be overriden by OperandCycles.

542    

543       // On the Atom, the throughput for taken branches is 2 cycles. For small

544       // simple loops, expand by a small factor to hide the backedge cost.

545       let LoopMicroOpBufferSize = 10;

546       let PostRAScheduler = 1;

547    

548       let Itineraries = AtomItineraries;       <-- v7.0删除

             let CompleteModel = 0;                    <-- v7.0增加

549     }

以下对v7.0部分适用)548行的AtomItineraries是一组庞大的定义(X86ScheduleAtom.td)。Atom依赖它来描述各种指令对资源(功能单元)占用的情况。能这么做,是因为Atom使用顺序流水线,而且只有两个Port口(这是Intel的处理器手册定义的功能单元,手册上就这么定义的)。

20       def Port0 : FuncUnit; // ALU: ALU0, shift/rotate, load/store

21                             // SIMD/FP: SIMD ALU, Shuffle,SIMD/FP multiply, divide

22       def Port1 : FuncUnit; // ALU: ALU1, bit processing, jump, and LEA

23                             // SIMD/FP: SIMD ALU, FP Adder

24      

25       def AtomItineraries : ProcessorItineraries<

26         [ Port0, Port1 ],

27         [], [

28         // P0 only

29         // InstrItinData] >,

30         // P0 or P1

31         // InstrItinData] >,

32         // P0 and P1

33         // InstrItinData,  InstrStage] >,

34         //

35         // Default is 1 cycle, port0 or port1

36         InstrItinDataInstrStage<1, [Port0]>] >,

37         InstrItinData] >,

38         InstrItinData] >,

39         InstrItinData] >,

40         // mul

             …

238       InstrItinData] >,

239    

240       InstrItinData] >,

241       InstrItinData] >,

242    

243       InstrItinData] >,

244       InstrItinData] >,

245    

246       InstrItinData] >,

247       InstrItinData] >,

248       InstrItinData] >,

249    

250       InstrItinData] >,

251       InstrItinData] >,

252       InstrItinData] >,

253    

254       InstrItinData] >,

255       InstrItinData] >,

256       InstrItinData] >,

257    

258       InstrItinData] >,

259    

260       InstrItinData] >,

261    

262       InstrItinData] >,

263       InstrItinData] >,

264       InstrItinData] >,

              …

357       InstrItinData, InstrStage<5, [Port1]>] >,

358       InstrItinData] >,

359       InstrItinData] >,

360    

361       InstrItinData] >,

362       InstrItinData] >,

363       InstrItinData] >,

             …

533       InstrItinData] >

534       ]>;

X86ScheduleAtom.td文件开头的注释提到,这部分定义来自“Intel 64 and IA32 Architectures Optimization Reference Manual”的第13章,第4节(2016年版则在第14章,第4节)。这份文档网上可以下载。文档中将这两个ALU命名为Port0与Port1,LLVM也遵循它的命名,在20与22行给出这两个定义。文档在第13章,第4节给出了一张表各种指令与ALU绑定及执行时延等信息,AtomItineraries正是根据这张表来构建的。比如,第36、37行的定义来自表的以下两项:

Instruction

Ports

Latency

Throughput

ADD/AND/CMP/OR/SUB/XOR/TEST mem, reg;

ADD/AND/CMP/OR/SUB/XOR2 reg, mem;

0

1

1

ADD/AND/CMP/OR/SUB/XOR reg, Imm8

ADD/AND/CMP/OR/SUB/XOR reg, imm

(0, 1)

1

0.5

现在我们看一下具体的例子。比如下面的指令定义(X86InstrCompiler.td):

551     multiclass LOCK_ArithBinOp RegOpc, bits<8> ImmOpc, bits<8> ImmOpc8,

552                                Format ImmMod, string mnemonic> {

553     let Defs = [EFLAGS], mayLoad = 1, mayStore = 1, isCodeGenOnly = 1,

554         SchedRW = [WriteALULd, WriteRMW] in {

555    

556     def NAME#8mr : I<{RegOpc{7}, RegOpc{6}, RegOpc{5}, RegOpc{4},

557                       RegOpc{3}, RegOpc{2}, RegOpc{1}, 0 },

558                     MRMDestMem, (outs), (ins i8mem:$dst, GR8:$src2),

559                       !strconcat(mnemonic, "{b}\t",

560                                  "{$src2, $dst|$dst, $src2}"),

561                       [], IIC_ALU_NONMEM>, LOCK;

562     def NAME#16mr : I<{RegOpc{7}, RegOpc{6}, RegOpc{5}, RegOpc{4},

563                        RegOpc{3}, RegOpc{2}, RegOpc{1}, 1 },

564                        MRMDestMem, (outs), (ins i16mem:$dst, GR16:$src2),

565                        !strconcat(mnemonic, "{w}\t",

566                                   "{$src2, $dst|$dst, $src2}"),

567                        [], IIC_ALU_NONMEM>, OpSize16, LOCK;

568     def NAME#32mr : I<{RegOpc{7}, RegOpc{6}, RegOpc{5}, RegOpc{4},

569                        RegOpc{3}, RegOpc{2}, RegOpc{1}, 1 },

570                        MRMDestMem, (outs), (ins i32mem:$dst, GR32:$src2),

571                        !strconcat(mnemonic, "{l}\t",

572                                   "{$src2, $dst|$dst, $src2}"),

573                        [], IIC_ALU_NONMEM>, OpSize32, LOCK;

574     def NAME#64mr : RI<{RegOpc{7}, RegOpc{6}, RegOpc{5}, RegOpc{4},

575                         RegOpc{3}, RegOpc{2}, RegOpc{1}, 1 },

576                         MRMDestMem, (outs), (ins i64mem:$dst, GR64:$src2),

577                         !strconcat(mnemonic, "{q}\t",

578                                    "{$src2, $dst|$dst, $src2}"),

579                         [], IIC_ALU_NONMEM>, LOCK;

580    

581     def NAME#8mi : Ii8<{ImmOpc{7}, ImmOpc{6}, ImmOpc{5}, ImmOpc{4},

582                         ImmOpc{3}, ImmOpc{2}, ImmOpc{1}, 0 },

583                         ImmMod, (outs), (ins i8mem :$dst, i8imm :$src2),

584                         !strconcat(mnemonic, "{b}\t",

585                                    "{$src2, $dst|$dst, $src2}"),

586                         [], IIC_ALU_MEM>, LOCK;

587    

588     def NAME#16mi : Ii16<{ImmOpc{7}, ImmOpc{6}, ImmOpc{5}, ImmOpc{4},

589                           ImmOpc{3}, ImmOpc{2}, ImmOpc{1}, 1 },

590                           ImmMod, (outs), (ins i16mem :$dst, i16imm :$src2),

591                           !strconcat(mnemonic, "{w}\t",

592                                      "{$src2, $dst|$dst, $src2}"),

593                           [], IIC_ALU_MEM>, OpSize16, LOCK;

594    

595     def NAME#32mi : Ii32<{ImmOpc{7}, ImmOpc{6}, ImmOpc{5}, ImmOpc{4},

596                           ImmOpc{3}, ImmOpc{2}, ImmOpc{1}, 1 },

597                           ImmMod, (outs), (ins i32mem :$dst, i32imm :$src2),

598                           !strconcat(mnemonic, "{l}\t",

599                                      "{$src2, $dst|$dst, $src2}"),

600                           [], IIC_ALU_MEM>, OpSize32, LOCK;

601    

602     def NAME#64mi32 : RIi32S<{ImmOpc{7}, ImmOpc{6}, ImmOpc{5}, ImmOpc{4},

603                               ImmOpc{3}, ImmOpc{2}, ImmOpc{1}, 1 },

604                               ImmMod, (outs), (ins i64mem :$dst, i64i32imm :$src2),

605                             !strconcat(mnemonic, "{q}\t",

606                                          "{$src2, $dst|$dst, $src2}"),

607                               [], IIC_ALU_MEM>, LOCK;

608    

609     def NAME#16mi8 : Ii8<{ImmOpc8{7}, ImmOpc8{6}, ImmOpc8{5}, ImmOpc8{4},

610                           ImmOpc8{3}, ImmOpc8{2}, ImmOpc8{1}, 1 },

611                           ImmMod, (outs), (ins i16mem :$dst, i16i8imm :$src2),

612                           !strconcat(mnemonic, "{w}\t",

613                                      "{$src2, $dst|$dst, $src2}"),

614                           [], IIC_ALU_MEM>, OpSize16, LOCK;

615     def NAME#32mi8 : Ii8<{ImmOpc8{7}, ImmOpc8{6}, ImmOpc8{5}, ImmOpc8{4},

616                           ImmOpc8{3}, ImmOpc8{2}, ImmOpc8{1}, 1 },

617                           ImmMod, (outs), (ins i32mem :$dst, i32i8imm :$src2),

618                           !strconcat(mnemonic, "{l}\t",

619                                      "{$src2, $dst|$dst, $src2}"),

620                           [], IIC_ALU_MEM>, OpSize32, LOCK;

621     def NAME#64mi8 : RIi8<{ImmOpc8{7}, ImmOpc8{6}, ImmOpc8{5}, ImmOpc8{4},

622                            ImmOpc8{3}, ImmOpc8{2}, ImmOpc8{1}, 1 },

623                            ImmMod, (outs), (ins i64mem :$dst, i64i8imm :$src2),

624                            !strconcat(mnemonic, "{q}\t",

625                                       "{$src2, $dst|$dst, $src2}"),

626                            [], IIC_ALU_MEM>, LOCK;

627    

628     }

629    

630     }

631    

632     defm LOCK_ADD : LOCK_ArithBinOp<0x00, 0x80, 0x83, MRM0m, "add">;

633     defm LOCK_SUB : LOCK_ArithBinOp<0x28, 0x80, 0x83, MRM5m, "sub">;

634     defm LOCK_OR  : LOCK_ArithBinOp<0x08, 0x80, 0x83, MRM1m, "or">;

635     defm LOCK_AND : LOCK_ArithBinOp<0x20, 0x80, 0x83, MRM4m, "and">;

636     defm LOCK_XOR : LOCK_ArithBinOp<0x30, 0x80, 0x83, MRM6m, "xor">;

在上面的定义里,RegOpc{7}表示访问RegOpc的第7位(RegOpc是一个bits<8>)。而像559行的!strconcat(mnemonic, "{b}\t", "{$src2, $dst|$dst, $src2}")则是这样的语义,比如632行的定义,mnemonic是add,那么!strconcat执行后得到:add{b}, {$src2, $dst|$dst, $src2},这包含了AT&T及Intel汇编形式,LLVM将根据需要输出:addb, $src2, $dst或add $dst, $src2。

LOCK_ArithBinOp是一个multiclass,从派生它将展开为多个定义,比如LOCK_ADD将展开为这些指令定义:LOCK_ADD8mr,LOCK_ADD16mr,LOCK_ADD32mr,LOCK_ADD64mr,LOCK_ADD8mi,LOCK_ADD16mi,LOCK_ADD32mi,LOCK_ADD64mi32,LOCK_ADD64mi8,LOCK_ADD16mi8,LOCK_ADD32mi8,LOCK_ADD64mi8。

这些指令定义的SchedRW被定义为[WriteALULd, WriteRMW],WriteALULd与WriteRMW都是SchedWrite的空派生def。它们表示这些指令所占用的资源,其中WriteALULd表示这些是寄存器-内存间简单的整形ALU操作,WriteRMW表示这些是执行读-修改-写的指令。但在Atom的定义里,资源的映射实际上是由IIC_ALU_MEM、IIC_ALU_NONMEM这样的InstrItinClass派生定义来实现的。指令的SchedRW定义,对Atom处理器是没有意义的。

V7.0使用与Sandy Bridge相同的方式来描述指令调度(从资源使用的角度),这里就不深入了。下面对Sandy Bridge的讨论也适用于v7.0Atom

​​​​​​​2.4.2.2. Sandy Bridge的描述

采用Intel SandyBridge架构的处理器要比Atom复杂得多,下图显示处理器的流水线与主要组件。

其流水线包括:

  • 一个获取指令并将它们解码为微操作的顺序发布前端。前端向下一个流水线阶段提供来自程序执行最可能路径的连续微操作流。
  • 一个乱序的、能每周期分发执行最多6个微操作的超标量执行引擎。分配/重命名块将微操作重排为“数据流”序,使得它们可以在资源就绪后尽快执行。
  • 一个确保微操作的执行结果,包括它们可能遇到任何异常,以原有程序顺序出现的顺序回收单元。

指令在流水线里的流动可以被总结为以下的动作:

  1. 分支预测单元从程序选择下一个要执行的代码块。处理器在以下资源中以这个次序查找代码:
  1. 已解码 ICache
  2. 指令Cache,通过激活遗留解码流水线
  3. 如果需要,L2 Cache、末级缓存(llc)以及内存
  1. 代码对应的微操作被发送到重命名/回收单元。它们以程序序进入调度器,但根据数据流序从调度器执行与释放。至于同时就绪微操作,几乎总是维持FIFO序。

由安排在三个栈上的执行资源执行微操作。每个栈上的执行单元与指令的数据类型相关。

分支执行触发分支预测。它将发布微操作的前端重导向到正确的路径。处理器可以用后面正确路径的操作覆盖掉在分支误判之前的操作。

  1. 管理及重排内存操作以实现并发性及最大的性能。L1数据Cache不命中的数据将进入L2 Cache。数据Cache是非阻塞的,可以处理多个并发的不命中。
  2. 在出错指令退出时(或尝试退出时)触发异常(Fault,Trap)。

Sandy Bridge微架构的整体情况由下面的SchedMachineModel派生定义来描述。比如一周期能发布、执行4个微操作,微操作重排队列增大到168,而且还有循环微操作缓冲(用于循环流检测)。

15       def SandyBridgeModel : SchedMachineModel {

16         // All x86 instructions are modeled as a single micro-op, and SB can decode 4

17         // instructions per cycle.

18         // FIXME: Identify instructions that aren't a single fused micro-op.

19         let IssueWidth = 4;

20        let MicroOpBufferSize = 168; // Based on the reorder buffer.

21         let LoadLatency = 4;                  <-- v7.0改为5

22         let MispredictPenalty = 16;

23      

24         // Based on the LSD (loop-stream detector) queue size.

25         let LoopMicroOpBufferSize = 28;

26      

27         // FIXME: SSE4 and AVX are unimplemented. This flag is set to allow

28         // the scheduler to assign a default model to unrecognized opcodes.

29         let CompleteModel = 0;

30       }

实际上,Sandy Bridge微架构相当复杂,难以在处理器层次对指令的执行进行详细描述。因此,它更多是通过定义WriteRes与ReadAdvance将处理器资源及时延关联到每个SchedReadWrite定义,来描述指令的执行。

​​​​​​​2.4.2.2.1. 资源的描述

在LLVM里对Sandy Bridge执行资源的描述在X86SchedSandyBridge.td。在进入具体定义之前,我们先看一下描述资源所需的定义。首先是ProcResourceUnits:

165     class ProcResourceUnits {

166       ProcResourceKind Kind = kind;

167       int NumUnits = num;

168       ProcResourceKind Super = ?;

169       int BufferSize = -1;

170       SchedMachineModel SchedModel = ?;

171     }

它定义了若干可互换的处理器资源。NumUnits确定要求该资源指令的吞吐率。

可以给出一个可选的超级资源把这些资源模拟为更一般超级资源的一个子集。使用这些资源中的一个暗示着使用其中一个超级资源。

ProcResourceUnits通常在一个乱序引擎里构建几个被缓冲的资源。被缓冲的资源可以被持有几个时钟周期,但调度器不会把它们固定在相对于指令分发的一个特定时钟周期。设置BufferSize为0表示一个顺序发布的资源。在这个情形下,调度器从顺序发出指令的周期倒数,一旦后续指令要求相同的资源,强制暂停,直到WriteRes中指定的ResourceCyles个周期过去。设置BufferSize为1表示一个顺序时延资源。在这个情形下,调度器在使用这个资源的指令之间建立生产者/消费者暂停。

例子(都假定一个乱序引擎):

对由一个一体式保留站(unified reservation station)填充的“发布端口”使用BufferSize = -1(保留站的作用是排队微操作,直到所有源操作数就绪,将就绪的微操作调度并分发到可用的执行单元)。这里保留站的尺寸由MicroOpBufferSize给出,MicroOpBufferSize应该是寄存器重命名池、一体式保留站、或重排缓冲中的最小尺寸。

对强制“成组发布(dispatch/issue groups)”的资源使用BufferSize = 0(不同的处理器定义以不同的方式定义发布。这里指解码为微操作以及移入保留站之间的阶段)。通常NumMicroOps足以限制组发布。不过,一些处理器可以构成仅有特定指令类型组合的组,比如POWER7。

对顺序执行单元使用BufferSize = 1。这用于一个乱序核内的一个顺序流水线,其中依赖调度的操作保证靠在一起,并生成一个空泡,比如Cortex-a9浮点单元。

对带有独立保留站的乱序执行单元使用BufferSize > 1。这只是模拟了该保留站的大小。

为了模拟发布组与顺序执行单元,创建两个单元类型,一个BufferSize=0,另一个BufferSize=1。

170行的SchedModel用于将资源单元绑定到处理器(参考下面Sandy Bridge的定义)。

165行的参数ProcResourceKind只是一个空的class,类似于C++里的抽象基类,代表处理器资源类型。因此处理器资源的定义是这样的,num表示资源的个数:

180     class ProcResource : ProcResourceKind,

181       ProcResourceUnits;

其中EponymousProcResourceKind是一个空的def:

176     def EponymousProcResourceKind : ProcResourceKind;

据说它主要是为了能让ProcResourceUnits定义能援引它自己,不过我看不出这是为什么。

多种资源还能构成资源组。

183     class ProcResGroupProcResource> resources> : ProcResourceKind {

184       list Resources = resources;

185       SchedMachineModel SchedModel = ?;

186       int BufferSize = -1;

187     }

另外,对写操作专门定义了一个基类ProcWriteResources:

233  class ProcWriteResources resources> {

234       list ProcResources = resources;

235       list ResourceCycles = [];

236       int Latency = 1;

237       int NumMicroOps = 1;

238       bit BeginGroup = 0;

239       bit EndGroup = 0;

240       // Allow a processor to mark some scheduling classes as unsupported

241       // for stronger verification.

242       bit Unsupported = 0;

             <-- v7.0增加

             // Allow a processor to mark some scheduling classes as single-issue.

             // SingleIssue is an alias for Begin/End Group.

             bit SingleIssue = 0;

243       SchedMachineModel SchedModel = ?;

244     }

注意,资源一定属于某个处理器,因此定义中总是有SchedMachineModel来将它们绑定到指定的处理器。

2.4.2.2.2. ​​​​​​​执行的描述

首先是SchedReadWrite的定义,同样作为类似于抽象基类的成分,它是一个空的class。对Sandy Bridge这样具有乱序执行微操作能力的处理器来说,原则上每个SchedReadWrite派生定义对应一个微操作执行。

207     // Define a scheduler resource associated with a def operand.

208     class SchedWrite : SchedReadWrite;

209     def NoWrite : SchedWrite;

210    

211     // Define a scheduler resource associated with a use operand.

212     class SchedRead : SchedReadWrite;

将ProcWriteResources派生定义绑定到资源有两种方式,分别是WriteRes与SchedWriteRes。

ProcWriteResources是WriteRes与SchedWriteRes的基类。

280     class WriteRes<SchedWrite write, list resources>

281       : ProcWriteResources {

282       SchedWrite WriteType = write;

283     }

 

288     class SchedWriteRes resources> : SchedWrite,

289       ProcWriteResources;

WriteRes定义了资源以及一个SchedWrite(微操作)的时延。这将由没有itinerary类的目标机器直接使用。在这个情形下,SchedWrite由目标机器定义,而ProcWriteResources由次级目标机器定义,并且将SchedWrite映射到处理器资源(参见Sandy Bridge的定义)。

如果目标机器已经有itinerary类,可以使用ProcWriteResources(原作ProcWriteResources)来定义子目标机器特定的SchedWrites,并将它们一起映射到处理器资源。然后ItinRW可以将itinerary类映射到次级目标机器的SchedWrites。

ProcResources(类ProcWriteResources的成员,后面的ResourceCycles也是)代表由写操作消耗的资源组。可选地,ResourceCycles用于表示资源被消费的周期数。ResourceCycles可以是“[]”:在这个情形里,对单个周期消耗所有资源,不管时延,这模拟了一个完全流水线化的处理单元。ResourceCycles值为0意味着资源必须可用但不消耗,这仅与非缓冲资源相关。

默认的,每个SchedWrite耗费一个微操作,这根据处理器的IssueWidth限制来计数。如果一条指令可以在单个微操作里写多个寄存器,次级目标机器应该将其中一个写定义为零个微操作。如果次级目标机器要求多个微操作写同一个结果,应该要么将写的NumMicroOps改写为大于1,或要求另外的写操作。可以通过定义一个WriteSequence来定义额外的写,或只是在指令的写入者列表里列出“def”操作数以外的额外写。调度器假定所有的微操作必须在同一个周期里发布。可以要求这些微操作来启动或终止当前的发布组。

SchedWriteRes的作用与WriteRes类似,不过它是SchedWrite的派生定义,因此它可以直接用在SchedReadWrite定义能使用的地方。SchedWriteRes定义了一个新的SchedWrite类型,与此同时将它关联到一组WriteResources。这个新的SchedWrite类不知道它的SchedModel,所以必须由InstRW或ItinRW来引用它。

最后一个与写相关的定义是WriteSequence。它将一个SchedWrite构建为一组带有累计时延的SchedWrite。这相当于将一个操作数映射到由前面定义的一组SchedWrite组成的资源。

224     class WriteSequence writes, int rep = 1> : SchedWrite {

225       list Writes = writes;

226       int Repeat = rep;

227       SchedMachineModel SchedModel = ?;

228     }

如果这个序列中最后的写被标记为Variadic,在解析了最后写的谓词之后,前面的写分散在所有的操作数上。

对于读操作,则有这些资源相关定义。首先是ProcReadAdvance:

294     class ProcReadAdvanceSchedWrite> writes = []> {

295       int Cycles = cycles;

296       list ValidWrites = writes;

297       // Allow a processor to mark some scheduling classes as unsupported

298       // for stronger verification.

299       bit Unsupported = 0;

300       SchedMachineModel SchedModel = ?;

301     }

其中的SchedModel将这些定义的资源绑定到处理器。从ProcReadAdvance派生出ReadAdvance与SchedReadAdvance。

313     class ReadAdvance<SchedRead read, int cycles, list<SchedWrite> writes = []>

314       : ProcReadAdvance {

315       SchedRead ReadType = read;

316     }

处理器可以定义与一个SchedRead关联的ReadAdvance来将前面一个写的时延减少N个周期。一个负的进展实际上增加了时延,这可能用于跨领域暂停(cross-domain stall)。

ReadAdvance可与一组SchedWrite关联以实现流水线旁路。写列表可以是空的来表示总是比普通寄存器延迟cycles周期的读入操作数,允许读的父指令相对于写入者更早地发布它。

320     class SchedReadAdvanceSchedWrite> writes = []> : SchedRead,

321       ProcReadAdvance;

SchedReadAdvance则直接将一个新的SchedRead类型与一个时延及可选的流水线旁路关联。与InstRW或ItinRW一起使用。

我们已经知道在Instruction的定义里,SchedRW(类型list)给出了每个操作数所占用的资源的描述。另外,对那些子目标机器间存在较大差异的目标机器,比如ARM,LLVM提供了一个语法糖果InstRW用以将一组指令重新绑定到另一组SchedReadWrite定义。

391     class InstRW rw, dag instrlist> {

392       list OperandReadWrites = rw;

393       dag Instrs = instrlist;

394       SchedMachineModel SchedModel = ?;

395     }

这是ARM的一个例子(AArch64SchedA53.td):

204     def : InstRW<[A53WriteVLD1], (instregex "LD1i(8|16|32|64)$")>;

在这个定义里以LD1i8、LD1i16、LD1i32及LD1i64开头的指令被重新绑定到A53WriteVLD1。同样,instregex是另一个TableGen支持的内置关键字,是用于描述指令的正则表达式。它也是由TableGen根据表达式自己展开。

另一个语法糖果目前也是给ARM家族的。ItinRW将一组InstrItinClass映射为一组SchedReadWrite。

402     class ItinRW rw, list iic> {

403       list MatchedItinClasses = iic;

404       list OperandReadWrites = rw;

405       SchedMachineModel SchedModel = ?;

406     }

这是ARM的一个例子(ARMScheduleA9.td)

2264  def :ItinRW<[WriteALU, A9ReadALU],[IIC_iMVNr]>;

表示步骤IIC_iMVNr使用资源WriteALU与A9ReadALU。

最后一颗糖果是SchedAlias,子目标机器可以通过它将一个SchedReadWrite映射到一个WriteSequence、SchedWriteVariant或者SchedReadVariant。

415     class SchedAlias {

416       SchedReadWrite MatchRW = match;

417       SchedReadWrite AliasRW = alias;

418       SchedMachineModel SchedModel = ?;

419     }

这是ARM的一个例子(AArch64SchedA53.td):

169     def : SchedAlias;

其中A53ReadISReg是一个SchedReadVariant派生定义:

166     def A53ReadISReg : SchedReadVariant<[

167          SchedVar,

168          SchedVar<NoSchedPred, [A53ReadNotShifted]>]>;

SchedReadVariant派生自SchedRead与SchedVariant。它将一个SchedRead映射到一组SchedRead,以这组SchedRead给出的谓词为条件,从中选择符合条件的SchedRead定义。

383     class SchedReadVariantSchedVar> variants> : SchedRead,

384       SchedVariant {

385     }

其中基类SchedVariant的定义为:

361     class SchedVariantSchedVar> variants> {

362       list Variants = variants;

363       bit Variadic = 0;

364       SchedMachineModel SchedModel = ?;

365     }

定义中的SchedVar列表定义了这个选择过程。默认的,选中的SchedReadWrites仍然与一个操作数关联并假定以累计的时延顺序执行。不过,如果父SchedWriteVariant或SchedReadVariant被标记为“Variadic”,那么每个选中SchedReadWrite被适当地映射到该指令的可变操作数。在这个情形下,时延不是累加的。如果当前变量已经是一个Sequence的部分,那么在该变量之前的部分应用在可变操作数上。

355     class SchedVar<SchedPredicate pred, list selected> {

356       SchedPredicate Predicate = pred;                                                                                <-- v7.0删除

  SchedPredicateBase Predicate = pred;                                                                                   <-- v7.0增加

357       list Selected = selected;

358     }

另外356行的SchedPredicate是这样定义的(344行是它的一个特殊派生def,表示没有谓词,即永远选中):

340     class SchedPredicate {

341       SchedMachineModel SchedModel = ?;

342       code Predicate = pred;

343     }

344     def NoSchedPred: SchedPredicate<[{true}]>;

V7.0SchedVar中使用SchedPredicateBase替换了SchedPredicate。这是因为在v7.0里除了SchedPredicate,还有一个SchedPredicateBase的派生定义,MCSchedPredicate,它可以替换SchedPredicate,专门用于选择MachineInstr或者MCInst。它是这样的定义:

362     class MCSchedPredicate : SchedPredicateBase {

363       MCInstPredicate Pred = P;

364       SchedMachineModel SchedModel = ?;

365     }

X86里这样的例子有:

599     def JWriteZeroIdiom : SchedWriteVariant<[

600         SchedVar, [JWriteZeroLatency]>,

601         SchedVar,           [WriteALU]>

602     ]>;

某些对源操作数都使用同一个寄存器的指令对该寄存器之前的内容没有真正的依赖,因此在完成前无需等待。它们可以在寄存器重命名阶段时优化掉。JwriteZeroIdiom用于找出这样的指令。

看到A53ReadISReg给出了两个SchedRead。在满足谓词RegShiftedPred时,选中A53ReadShifted,否则选中A53ReadNotShifted。

而ReadISReg则是一个平凡的定义:

29       def ReadISReg: SchedRead;  // ALU of Shifted-Reg

现在它是A53ReadISReg的别名。如果某条指令使用了ReadISReg,那么在执行ReadISReg时,是从A53ReadISReg中根据谓词选择A53ReadShifted或A53ReadNotShifted来执行。更多关于ItinRW以及SchedVariant、SchedVar的内容,参考从ItinRW定义推导一节。

2.4.2.2.3. ​​​​​​​Sandy Bridge的定义

SandyBridgeModel是没有Itinerary的(而Atom则主要通过Itinerary来描述,v7.0已不是这样),因为Sandy Bridge先将指令翻译为一系列微操作,并具有乱序执行微操作的能力,可同时执行不同指令的微操作。因此Sandy Bridge(及其后续CPU)使用一系列SchedReadWrite派生定义来描述对操作数的各种操作(大致可视为微操作的执行),并通过各种WriteRes派生定义关联这些操作所需的资源。

Sandy Bridge的相关定义如下。在所有这些定义里用到的SchedWrite都是X86FoldableSchedWrite的派生定义。X86FoldableSchedWrite是这样定义的:

27       class X86FoldableSchedWrite : SchedWrite {

28         // The SchedWrite to use when a load is folded into the instruction.

29         SchedWrite Folded;

30       }

大多数指令有可以直接操作内存的版本,因此几乎每个SchedWrite都有两个变种:操作寄存器或操作内存(操作内存的版本带有Ld后缀)。X86SchedWritePair是定义这些操作对的一个方便的基类:

33       multiclass X86SchedWritePair {

34         // Register-Memory operation.

35         def Ld : SchedWrite;

36         // Register-Register operation.

37         def NAME : X86FoldableSchedWrite {

38           let Folded = !cast(NAME#"Ld");

39         }

40       }

37行的NAME是TableGen中一个特殊的标识符,它代表X86SchedWritePair派生定义的名字。比如下面43行的WriteALU,这时NAME就是WriteALU。但35行的定义则会生成WriteALULd这个名字的定义(在前面LOCK_ArithBinOp定义可以看到其中的SchedRW被定义为 [WriteALULd, WriteRMW])。使用X86SchedWritePair的定义集中在X86Schedule.td,比如:

42       // Arithmetic.

43      defm WriteALU: X86SchedWritePair; // Simple integer ALU op.

44       defm WriteIMul : X86SchedWritePair; // Integer multiplication.

45       def  WriteIMulH : SchedWrite;       // Integer multiplication, high part.

46       defm WriteIDiv : X86SchedWritePair; // Integer division.

47       def  WriteLEA  : SchedWrite;        // LEA instructions can't fold loads.

48      

49       // Integer shifts and rotates.

50       defm WriteShift : X86SchedWritePair;

51      

52       // Loads, stores, and moves, not folded with other operations.

53       def WriteLoad  : SchedWrite;

54       def WriteStore : SchedWrite;

55       def WriteMove  : SchedWrite;

类似于X86SchedWritePair,下面72行的SBWriteResPair是封装类似WriteALU与WriteALULd这样的操作对与相关资源的一个便利方法。从定义可以看出,执行内存操作的版本比寄存器版本的时延要多4处理器周期v7.0有一个更清晰的定义)

multiclass SBWriteResPair

                          list ExePorts,

                          int Lat, list Res = [1], int UOps = 1,

                          int LoadLat = 5> {

  // Register variant is using a single cycle on ExePort.

  def : WriteRes {

    let Latency = Lat;

    let ResourceCycles = Res;

    let NumMicroOps = UOps;

  }

 

  // Memory variant also uses a cycle on port 2/3 and adds LoadLat cycles to

  // the latency (default = 5).

  def : WriteRes {

    let Latency = !add(Lat, LoadLat);

    let ResourceCycles = !listconcat([1], Res);

    let NumMicroOps = !add(UOps, 1);

  }

})。

在下面首先定义了Sandy Bridge的资源。与Atom将Port0、Port1都定义为功能单元(FuncUnit)不同,Sandy Bridge将Port1~Port5都定义为资源,并根据使用情况定义了资源组(ProcResource后面的数字表示资源的数目,一个ProcResource派生def同时也是一个新的ProcResourceKind)。这是必然的,因为在Sandy Bridge里,指令只要使用不同的资源,且没有依赖关系,就能并发执行,指令能否调度很大程度上取决于是否有可用资源。从资源角度描述处理器能更好地支持指令调度。57行的BufferSize实际上是Sandy Bridge保留站(reservation station )的深度。

V7.0相当大地修改了下面的定义,比较显著的是将时延从4改到了5。另外,许多操作的资源使用情况也更新了(从v3.6250行大幅增加到1159行)。这里,不一一列举。

32       let SchedModel = SandyBridgeModel in {

33      

34       // Sandy Bridge can issue micro-ops to 6 different ports in one cycle.

35      

36       // Ports 0, 1, and 5 handle all computation.

37       def SBPort0 : ProcResource<1>;

38       def SBPort1 : ProcResource<1>;

39       def SBPort5 : ProcResource<1>;

40      

41       // Ports 2 and 3 are identical. They handle loads and the address half of

42       // stores.

43       def SBPort23 : ProcResource<2>;

44      

45       // Port 4 gets the data half of stores. Store data can be available later than

46       // the store address, but since we don't model the latency of stores, we can

47       // ignore that.

48       def SBPort4 : ProcResource<1>;

49      

50       // Many micro-ops are capable of issuing on multiple ports.

51       def SBPort05  : ProcResGroup<[SBPort0, SBPort5]>;

52       def SBPort15  : ProcResGroup<[SBPort1, SBPort5]>;

53       def SBPort015 : ProcResGroup<[SBPort0, SBPort1, SBPort5]>;

54      

55       // 54 Entry Unified Scheduler

56       def SBPortAny : ProcResGroup<[SBPort0, SBPort1, SBPort23, SBPort4, SBPort5]> {

57         let BufferSize=54;

58       }

59      

60       // Integer division issued on port 0.

61       def SBDivider : ProcResource<1>;

62      

63       // Loads are 4 cycles, so ReadAfterLd registers needn't be available until 4

64       // cycles after the memory operand.

65       def : ReadAdvance;

66      

67       // Many SchedWrites are defined in pairs with and without a folded load.

68       // Instructions with folded loads are usually micro-fused, so they only appear

69       // as two micro-ops when queued in the reservation station.

70       // This multiclass defines the resource usage for variants with and without

71       // folded loads.

72      multiclass SBWriteResPair<X86FoldableSchedWrite SchedRW,

73                                 ProcResourceKind ExePort,

74                                 int Lat> {

75         // Register variant is using a single cycle on ExePort.

76         def : WriteRes { let Latency = Lat; }

77      

78         // Memory variant also uses a cycle on port 2/3 and adds 4 cycles to the

79         // latency.

80         def : WriteRes {

81            let Latency = !add(Lat, 4);

82         }

83       }

84      

85       // A folded store needs a cycle on port 4 for the store data, but it does not

86       // need an extra port 2/3 cycle to recompute the address.

87       def : WriteRes;

88      

89       def : WriteRes;

90       def : WriteRes { let Latency = 4; }

91       def : WriteRes;

92       def : WriteRes;

93      

94       defm : SBWriteResPair;

95       defm : SBWriteResPair;

96       def  : WriteRes { let Latency = 3; }

97       defm : SBWriteResPair;

98       defm : SBWriteResPair;

99      

100     // This is for simple LEAs with one or two input operands.

101     // The complex ones can only execute on port 1, and they require two cycles on

102     // the port to read all inputs. We don't model that.

103     def : WriteRes;

104    

105     // This is quite rough, latency depends on the dividend.

106     def : WriteRes {

107       let Latency = 25;

108       let ResourceCycles = [1, 10];

109     }

110     def : WriteRes {

111       let Latency = 29;

112       let ResourceCycles = [1, 1, 10];

113     }

114    

115     // Scalar and vector floating point.

116     defm : SBWriteResPair;

117     defm : SBWriteResPair;

118     defm : SBWriteResPair; // 10-14 cycles.

119     defm : SBWriteResPair;

120     defm : SBWriteResPair;

121     defm : SBWriteResPair;

122     defm : SBWriteResPair;

123     defm : SBWriteResPair;

124     defm : SBWriteResPair;

125     defm : SBWriteResPair;

126     defm : SBWriteResPair;

127     def : WriteRes {

128       let Latency = 2;

129       let ResourceCycles = [1, 1];

130     }

131     def : WriteRes {

132       let Latency = 6;

133       let ResourceCycles = [1, 1, 1];

134     }

135    

136     // Vector integer operations.

137     defm : SBWriteResPair;

138     defm : SBWriteResPair;

139     defm : SBWriteResPair;

140     defm : SBWriteResPair;

141     defm : SBWriteResPair;

142     defm : SBWriteResPair;

143     def : WriteRes {

144       let Latency = 2;

145       let ResourceCycles = [1, 1];

146     }

147     def : WriteRes {

148       let Latency = 6;

149       let ResourceCycles = [1, 1, 1];

150     }

151     def : WriteRes {

152       let Latency = 6;

153       let ResourceCycles = [1, 1, 1];

154     }

155     def : WriteRes {

156       let Latency = 6;

157       let ResourceCycles = [1, 1, 1, 1];

158     }

159    

160     // String instructions.

161     // Packed Compare Implicit Length Strings, Return Mask

162     def : WriteRes {

163       let Latency = 11;

164       let ResourceCycles = [3];

165     }

166     def : WriteRes {

167       let Latency = 11;

168       let ResourceCycles = [3, 1];

169     }

170    

171     // Packed Compare Explicit Length Strings, Return Mask

172     def : WriteRes {

173       let Latency = 11;

174       let ResourceCycles = [8];

175     }

176     def : WriteRes {

177       let Latency = 11;

178       let ResourceCycles = [7, 1];

179     }

180    

181     // Packed Compare Implicit Length Strings, Return Index

182     def : WriteRes {

183       let Latency = 3;

184       let ResourceCycles = [3];

185     }

186     def : WriteRes {

187       let Latency = 3;

188       let ResourceCycles = [3, 1];

189     }

190    

191     // Packed Compare Explicit Length Strings, Return Index

192     def : WriteRes {

193       let Latency = 4;

194       let ResourceCycles = [8];

195     }

196     def : WriteRes {

197       let Latency = 4;

198       let ResourceCycles = [7, 1];

199     }

200    

201     // AES Instructions.

202     def : WriteRes {

203      let Latency = 8;

204       let ResourceCycles = [2];

205     }

206     def : WriteRes {

207       let Latency = 8;

208       let ResourceCycles = [2, 1];

209     }

210    

211     def : WriteRes {

212       let Latency = 8;

213       let ResourceCycles = [2];

214     }

215     def : WriteRes {

216       let Latency = 8;

217       let ResourceCycles = [2, 1];

218     }

219    

220     def : WriteRes {

221       let Latency = 8;

222       let ResourceCycles = [11];

223     }

224     def : WriteRes {

225       let Latency = 8;

226       let ResourceCycles = [10, 1];

227     }

228    

229     // Carry-less multiplication instructions.

230     def : WriteRes {

231       let Latency = 14;

232       let ResourceCycles = [18];

233     }

234     def : WriteRes {

235       let Latency = 14;

236       let ResourceCycles = [17, 1];

237     }

238    

239    

240     def : WriteRes { let Latency = 100; }

241     def : WriteRes { let Latency = 100; }

242     def : WriteRes;

243     def : WriteRes;

244    

245     // AVX2 is not supported on that architecture, but we should define the basic

246     // scheduling resources anyway.

247     defm : SBWriteResPair;

248     defm : SBWriteResPair;

249     defm : SBWriteResPair;

250     } // SchedModel

在上面的定义中Sandy Bridge将各种读写操作绑定到资源。比如87行WriteRes定义将WriteRMW(代表读-修改-写)绑定到SBPort4,表示该操作时延为一个周期,占用SBPort4一个周期。在94行将WriteALULd(代表寄存器内存间简单的整形ALU操作)绑定到资源SBPort015,表示该操作时延为一个周期,占用SBPort0、SBPort1或SBPort5。

现在需要有一个方法将指令的操作数与表示其相关操作的SchedReadWrite定义关联起来。这就是下面的Sched类(注意Instruction定义中有相同的SchedRW域,如果指令从Sched派生,TableGen在派生类中只会保留Sched中的SchedRW,即两者合二为一)。

203     class Sched schedrw> {

204       list SchedRW = schedrw;

205     }

在Sched定义里(包括Instruction的SchedRW),每个显式写入的操作数必须依次列出一个SchedWrite类型。对隐含写入的操作数列出额外的SchedWrite类型则是可选的。对读操作数列出SchedRead类型也是可选的。写相对于读的次序是无关重要的。这样,相同的SchedReadWrite序列可用于一个操作的多种形式。例如,一条双地址指令可以具有两个绑定的操作数或同时读写一个寄存器的单个操作数。在这两种情形下,有任意次序的单个SchedWrite与单个SchedRead。

以AVX类型的指令VCVTSS2SDrm为例(X86InstrSSE.td):

1884  let hasSideEffects = 0, Predicates = [UseAVX] in {

1885  def VCVTSS2SDrr : I<0x5A, MRMSrcReg, (outs FR64:$dst),

1886                      (ins FR32:$src1, FR32:$src2),

1887                      "vcvtss2sd\t{$src2, $src1, $dst|$dst, $src1, $src2}",

1888                      [], IIC_SSE_CVT_Scalar_RR>,

1889                      XS, Requires<[HasAVX]>, VEX_4V, VEX_LIG,

1890                      Sched<[WriteCvtF2F]>;

1891  let mayLoad = 1 in

1892  def VCVTSS2SDrm : I<0x5A, MRMSrcMem, (outs FR64:$dst),

1893                      (ins FR32:$src1, f32mem:$src2),

1894                      "vcvtss2sd\t{$src2, $src1, $dst|$dst, $src1, $src2}",

1895                      [], IIC_SSE_CVT_Scalar_RM>,

1896                      XS, VEX_4V, VEX_LIG, Requires<[HasAVX, OptForSize]>,

1897                      Sched<[WriteCvtF2FLd, ReadAfterLd]>;

1898  }

1897行的Sched作为VCVTSS2SDrm的基类之一,将WriteCvtF2FLd绑定到输出操作数FR64:$dst上,WriteCvtF2FLd代表一个浮点到浮点的转换。而ReadAfterLd则绑定到两个输入操作数,ReadAfterLd用于标记那些从内存读入的操作数(实际上直接设置Instruction中的SchedRW效果是相同的,参考前面LOCK_ArithBinOp的例子,它就是直接设置Instruction的SchedRW)。

同样注意1895行的IIC_SSE_CVT_Scalar_RM,它是为Atom这样的处理器准备的。

V7.0的定义改为:

let hasSideEffects = 0 in {

def VCVTSS2SDrr : I<0x5A, MRMSrcReg, (outs FR64:$dst),

                    (ins FR64:$src1, FR32:$src2),

                    "vcvtss2sd\t{$src2, $src1, $dst|$dst, $src1, $src2}", []>,

                    XS, VEX_4V, VEX_LIG, VEX_WIG,

                    Sched<[WriteCvtSS2SD]>, Requires<[UseAVX]>;

let mayLoad = 1 in

def VCVTSS2SDrm : I<0x5A, MRMSrcMem, (outs FR64:$dst),

                    (ins FR64:$src1, f32mem:$src2),

                    "vcvtss2sd\t{$src2, $src1, $dst|$dst, $src1, $src2}", []>,

                    XS, VEX_4V, VEX_LIG, VEX_WIG,

                    Sched<[WriteCvtSS2SD.Folded, ReadAfterLd]>,

                    Requires<[UseAVX, OptForSize]>;

}

注意WriteCvtSS2SD.Folded等同于WriteCvtSS2SDLDX86SchedWritePair定义里Folded指向它)。

​​​​​​​2.4.2.3. 总结

根据上面的讨论与例子。我们可以得出这些结论(这些内容实际来自LLVM代码中的注释)。通过定义SchedMachineModel,子目标机器可以给出下面三组数据之一:

    1. 粗粒度指令代价模型的基本属性。Target hooks允许子目标机器将操作码关联到这些属性。
    2. 用于简单每操作码代价模型调度器的读/写资源。可以下述方式的任意组合实现它:
  1. 通过修改指令定义使其继承自Sched,将每操作数的SchedReadWrite类型关联到指令。对于每个子目标机器,定义WriteRes与ReadAdvance将处理器资源与时延关联到每个SchedReadWrite类型。
  2. 在每条指令定义里,命名一个InstrItinClass。对于每个子目标机器,定义ItinRW项将InstrItinClass映射到每操作数的SchedReadWrite类型。不像方法A,这些类型可能是子目标机器特定的,可以通过定义SchedWriteRes及SchedReadAdvance,直接关联到资源。
  3. 在子目标机器里映射SchedReadWrite类型到特定的操作码。这覆盖该指令定义的任何SchedReadWrite类型或InstrItinClass。类似方法B,子目标机器通过定义SchedWriteRes及SchedReadAdvance,可以将SchedReadWrite类型直接关联到资源。
  4. 在目标机器或子目标机器里,定义SchedWriteVariant或SchedReadVariant,将一个SchedReadWrite类型映射到另一个SchedReadWrite类型序列。这允许通过定制的C++代码动态地选择一条指令的机器模型。它还允许将一个机器无关的SchedReadWrite类型映射到一个机器相关类型序列。
    1. 用于具体保留表的指令路线(Instruction itineraries)。可以通过提供Itineraries,外加将指令映射到InstrItinClass,实现一个每流水线步骤机器模型。

你可能感兴趣的:(LLVM学习笔记,compiler,编译器,llvm)