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
1065
1066 /// AssemblyParserVariants - The AsmParserVariant instances available for
1067 /// this target.
1068 list
1069
1070 // AssemblyWriters - The AsmWriter instances available for this target.
1071 list
<-- 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
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
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 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 1101 } 1100行的Implies表示,如果具有该特性,该特性隐含包含了哪些特性。 SubTargetFeature具有很好的灵活性以及描述能力,能够描述差异很大的处理器。以Atom处理器为例,这是Intel推出的面向移动设备的处理器。它与传统的Intel处理器相比,更像RISC处理器。TableGen这样定义它: 240 // Atom CPUs. 241 class BonnellProc 242 ProcIntelAtom, 243 FeatureSSSE3, 244 FeatureCMPXCHG16B, 245 FeatureMOVBE, 246 247 FeatureLeaForSP, 248 FeatureSlowDivide32, 249 FeatureSlowDivide64, 250 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 80 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 // // to skip the checks for scheduling information when building LLVM for // instructions which have any of the listed predicates in their Predicates // field. list 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 127 list 128 list 129 list 130 list 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 209 : ProcessorModel 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 242 ProcIntelAtom, 243 FeatureSSSE3, 244 FeatureCMPXCHG16B, 245 FeatureMOVBE, 246 247 FeatureLeaForSP, 248 FeatureSlowDivide32, 249 FeatureSlowDivide64, 250 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 258 ProcIntelSLM, 259 FeatureSSE42, 260 FeatureCMPXCHG16B, 261 FeatureMOVBE, 262 FeaturePOPCNT, 263 FeaturePCLMUL, 264 FeatureAES, 265 FeatureSlowDivide64, 266 267 FeaturePRFCHW, 268 FeatureSlowLEA, 269 FeatureSlowIncDec, 270 271 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 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 34 // 35 // Default is 1 cycle, port0 or port1 36 InstrItinData 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 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 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.0的Atom。 2.4.2.2. Sandy Bridge的描述 采用Intel SandyBridge架构的处理器要比Atom复杂得多,下图显示处理器的流水线与主要组件。 其流水线包括: 指令在流水线里的流动可以被总结为以下的动作: 由安排在三个栈上的执行资源执行微操作。每个栈上的执行单元与指令的数据类型相关。 分支执行触发分支预测。它将发布微操作的前端重导向到正确的路径。处理器可以用后面正确路径的操作覆盖掉在分支误判之前的操作。 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 181 ProcResourceUnits 其中EponymousProcResourceKind是一个空的def: 176 def EponymousProcResourceKind : ProcResourceKind; 据说它主要是为了能让ProcResourceUnits定义能援引它自己,不过我看不出这是为什么。 多种资源还能构成资源组。 183 class ProcResGroup 184 list 185 SchedMachineModel SchedModel = ?; 186 int BufferSize = -1; 187 } 另外,对写操作专门定义了一个基类ProcWriteResources: 233 class ProcWriteResources 234 list 235 list 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 281 : ProcWriteResources 282 SchedWrite WriteType = write; 283 } 288 class SchedWriteRes 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 225 list 226 int Repeat = rep; 227 SchedMachineModel SchedModel = ?; 228 } 如果这个序列中最后的写被标记为Variadic,在解析了最后写的谓词之后,前面的写分散在所有的操作数上。 对于读操作,则有这些资源相关定义。首先是ProcReadAdvance: 294 class ProcReadAdvance 295 int Cycles = cycles; 296 list 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 SchedReadAdvance 321 ProcReadAdvance SchedReadAdvance则直接将一个新的SchedRead类型与一个时延及可选的流水线旁路关联。与InstRW或ItinRW一起使用。 我们已经知道在Instruction的定义里,SchedRW(类型list 391 class InstRW 392 list 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 403 list 404 list 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 SchedReadVariant 384 SchedVariant 385 } 其中基类SchedVariant的定义为: 361 class SchedVariant 362 list 363 bit Variadic = 0; 364 SchedMachineModel SchedModel = ?; 365 } 定义中的SchedVar列表定义了这个选择过程。默认的,选中的SchedReadWrites仍然与一个操作数关联并假定以累计的时延顺序执行。不过,如果父SchedWriteVariant或SchedReadVariant被标记为“Variadic”,那么每个选中SchedReadWrite被适当地映射到该指令的可变操作数。在这个情形下,时延不是累加的。如果当前变量已经是一个Sequence的部分,那么在该变量之前的部分应用在可变操作数上。 355 class SchedVar<SchedPredicate pred, list 356 SchedPredicateBase Predicate = pred; <-- v7.0增加 357 list 358 } 另外356行的SchedPredicate是这样定义的(344行是它的一个特殊派生def,表示没有谓词,即永远选中): 340 class SchedPredicate 341 SchedMachineModel SchedModel = ?; 342 code Predicate = pred; 343 } 344 def NoSchedPred: SchedPredicate<[{true}]>; V7.0在SchedVar中使用SchedPredicateBase替换了SchedPredicate。这是因为在v7.0里除了SchedPredicate,还有一个SchedPredicateBase的派生定义,MCSchedPredicate,它可以替换SchedPredicate,专门用于选择MachineInstr或者MCInst。它是这样的定义: 362 class MCSchedPredicate 363 MCInstPredicate Pred = P; 364 SchedMachineModel SchedModel = ?; 365 } X86里这样的例子有: 599 def JWriteZeroIdiom : SchedWriteVariant<[ 600 SchedVar 601 SchedVar 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 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 int Lat, list 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.6的250行大幅增加到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 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 91 def : WriteRes 92 def : WriteRes 93 94 defm : SBWriteResPair 95 defm : SBWriteResPair 96 def : WriteRes 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 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 241 def : WriteRes 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 204 list 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等同于WriteCvtSS2SDLD(X86SchedWritePair定义里Folded指向它)。 2.4.2.3. 总结 根据上面的讨论与例子。我们可以得出这些结论(这些内容实际来自LLVM代码中的注释)。通过定义SchedMachineModel,子目标机器可以给出下面三组数据之一:FeatureSlowBTMem, <-- v7.0删除FeatureCallRegIndirect, <-- v7.0删除int MinLatency = -1; // Determines which instructions are allowed in a group. <-- v7.0删除 // (-1) inorder (0) ooo, (1): inorder +var latencies. fu, list
FeatureSlowBTMem, <-- v7.0删除FeatureCallRegIndirect, <-- v7.0删除FeatureCallRegIndirect, <-- v7.0删除FeatureSlowBTMem, <-- v7.0删除FeatureFastUAMem ß<-- v7.0删除let Itineraries = AtomItineraries; <-- v7.0删除
ProcResource> resources> : ProcResourceKind {
resources> {
resources> : SchedWrite,
writes, int rep = 1> : SchedWrite {
rw, dag instrlist> {
rw, list
SchedVar> variants> : SchedRead,
SchedVar> variants> {
SchedPredicate Predicate = pred; <-- v7.0删除 {
schedrw> {