LLVM学习笔记(18)
2017-09-08 11:45
363 查看
3.4.2.5. Instruction的处理
CodeGenDAGPatterns下一步开始处理指令定义。在Instruction的定义里有一个list<dag>类型的成员Pattern,如果不是空,它指定了该指令的匹配模式。在3072行获取Pattern的定义。3065 void
CodeGenDAGPatterns::ParseInstructions(){
3066 std::vector<Record*> Instrs =Records.getAllDerivedDefinitions("Instruction");
3067
3068
for (unsignedi = 0, e = Instrs.size(); i != e; ++i) {
3069 ListInit *LI = nullptr;
3070
3071 if(isa<ListInit>(Instrs[i]->getValueInit("Pattern")))
3072 LI =Instrs[i]->getValueAsListInit("Pattern");
3073
3074
// If there isno pattern, only collect minimal information about the
3075
// instructionfor its operand list. We have to assumethat there is one
3076
// result, aswe have no detailed info. A pattern which references the
3077
// null_fragoperator is as-if no pattern were specified. Normally this
3078
// is from amulticlass expansion w/ a SDPatternOperator passed in as
3079
// null_frag.
3080 if (!LI || LI->empty() ||hasNullFragReference(LI)) {
3081 std::vector<Record*> Results;
3082 std::vector<Record*> Operands;
3083
3084 CodeGenInstruction &InstInfo =Target.getInstruction(Instrs[i]);
3085
3086 if (InstInfo.Operands.size() != 0) {
3087
for(unsigned j = 0, e = InstInfo.Operands.NumDefs; j < e; ++j)
3088 Results.push_back(InstInfo.Operands[j].Rec);
3089
3090
// The restare inputs.
3091
for(unsigned j = InstInfo.Operands.NumDefs,
3092 e = InstInfo.Operands.size(); j< e; ++j)
3093 Operands.push_back(InstInfo.Operands[j].Rec);
3094 }
3095
3096
// Create andinsert the instruction.
3097 std::vector<Record*> ImpResults;
3098 Instructions.insert(std::make_pair(Instrs[i],
3099 DAGInstruction(nullptr, Results, Operands, ImpResults)));
3100
continue; // no pattern.
3101 }
3102
3103 CodeGenInstruction &CGI = Target.getInstruction(Instrs[i]);
3104
constDAGInstruction &DI = parseInstructionPattern(CGI,LI, Instructions);
3105
3106 (void)DI;
3107 DEBUG(DI.getPattern()->dump());
3108 }
3.4.5.2.1. 无匹配模式的Instruction
在TargetSelectionDAG.td文件里有一个null_frag的定义,专用于表示空模式。在一个展开的模式里,一旦援引了null_frag的定义,这个模式将被丢弃。这可以实现可选模式。那么,3080行的hasNullFragReference在模式中查找是否使用null_frag作为操作符,一旦找到就返回true。
如果Instruction定义没有定义Pattern(使用null_frag作为操作符也等同于没有Pattern),仅需要生成收集最少量参数列表信息的CodeGenInstruction实例。
157 CodeGenInstruction &getInstruction(const Record *InstRec)
const{
158 if (Instructions.empty())
ReadInstructions();
159
auto I =Instructions.find(InstRec);
160
assert(I !=Instructions.end() && "Not an instruction");
161
return*I->second;
162 }
Instructions是mutable DenseMap<const Record*,std::unique_ptr<CodeGenInstruction>>类型的容器(CodeGenTarget的成员),这时它一定是空的。因此,由下面的方法创建所有Instruction定义所对应的CodeGenInstruction实例。
267 void
CodeGenTarget::ReadInstructions()const {
268 std::vector<Record*> Insts =Records.getAllDerivedDefinitions("Instruction");
269 if (Insts.size() <= 2)
270 PrintFatalError("No 'Instruction'subclasses defined!");
271
272
// Parse theinstructions defined in the .td file.
273
for (unsignedi = 0, e = Insts.size(); i != e; ++i)
274 Instructions[Insts[i]] =llvm::make_unique<CodeGenInstruction>(Insts[i]);
275 }
274行调用了CodeGenInstruction的构造函数。
295 CodeGenInstruction::CodeGenInstruction(Record*R)
296 : TheDef(R), Operands(R),InferredFrom(nullptr) {
297 Namespace =R->getValueAsString("Namespace");
298 AsmString =R->getValueAsString("AsmString");
299
300 isReturn = R->getValueAsBit("isReturn");
301 isBranch = R->getValueAsBit("isBranch");
302 isIndirectBranch =R->getValueAsBit("isIndirectBranch");
303 isCompare = R->getValueAsBit("isCompare");
304 isMoveImm = R->getValueAsBit("isMoveImm");
305 isBitcast = R->getValueAsBit("isBitcast");
306 isSelect = R->getValueAsBit("isSelect");
307 isBarrier = R->getValueAsBit("isBarrier");
308 isCall = R->getValueAsBit("isCall");
309 canFoldAsLoad =R->getValueAsBit("canFoldAsLoad");
310 isPredicable = Operands.isPredicable ||R->getValueAsBit("isPredicable");
311 isConvertibleToThreeAddress =R->getValueAsBit("isConvertibleToThreeAddress");
312 isCommutable =R->getValueAsBit("isCommutable");
313 isTerminator =R->getValueAsBit("isTerminator");
314 isReMaterializable =R->getValueAsBit("isReMaterializable");
315 hasDelaySlot =R->getValueAsBit("hasDelaySlot");
316 usesCustomInserter =R->getValueAsBit("usesCustomInserter");
317 hasPostISelHook =R->getValueAsBit("hasPostISelHook");
318 hasCtrlDep = R->getValueAsBit("hasCtrlDep");
319 isNotDuplicable =R->getValueAsBit("isNotDuplicable");
320 isRegSequence =R->getValueAsBit("isRegSequence");
321 isExtractSubreg = R->getValueAsBit("isExtractSubreg");
322 isInsertSubreg =R->getValueAsBit("isInsertSubreg");
323 isConvergent =R->getValueAsBit("isConvergent");
324
325 bool Unset;
326 mayLoad = R->getValueAsBitOrUnset("mayLoad", Unset);
327 mayLoad_Unset = Unset;
328 mayStore = R->getValueAsBitOrUnset("mayStore", Unset);
329 mayStore_Unset = Unset;
330 hasSideEffects =R->getValueAsBitOrUnset("hasSideEffects", Unset);
331 hasSideEffects_Unset = Unset;
332
333 isAsCheapAsAMove =R->getValueAsBit("isAsCheapAsAMove");
334 hasExtraSrcRegAllocReq =R->getValueAsBit("hasExtraSrcRegAllocReq");
335 hasExtraDefRegAllocReq =R->getValueAsBit("hasExtraDefRegAllocReq");
336 isCodeGenOnly =R->getValueAsBit("isCodeGenOnly");
337 isPseudo =R->getValueAsBit("isPseudo");
338 ImplicitDefs =R->getValueAsListOfDefs("Defs");
339 ImplicitUses =R->getValueAsListOfDefs("Uses");
340
341
// ParseConstraints.
342
ParseConstraints(R->getValueAsString("Constraints"),Operands);
343
344
// Parse theDisableEncoding field.
345 Operands.ProcessDisableEncoding(R->getValueAsString("DisableEncoding"));
346
347
// First checkfor a ComplexDeprecationPredicate.
348 if(R->getValue("ComplexDeprecationPredicate")) {
349 HasComplexDeprecationPredicate = true;
350 DeprecatedReason =R->getValueAsString("ComplexDeprecationPredicate");
351 } else if (RecordVal *Dep = R->getValue("DeprecatedFeatureMask")){
352
// Check if wehave a Subtarget feature mask.
353 HasComplexDeprecationPredicate = false;
354 DeprecatedReason =Dep->getValue()->getAsString();
355 } else {
356
// Thisinstruction isn't deprecated.
357 HasComplexDeprecationPredicate = false;
358 DeprecatedReason = "";
359 }
360 }
296行的Operands是CGIOperandList类型的成员,因此在该行调用了CGIOperandList的构造函数,为Instruction定义中的操作数构建OperandInfo实例。
28
CGIOperandList::CGIOperandList(Record*R) : TheDef(R) {
29 isPredicable = false;
30 hasOptionalDef = false;
31 isVariadic = false;
32
33 DagInit *OutDI =R->getValueAsDag("OutOperandList");
34
35 if (DefInit *Init =dyn_cast<DefInit>(OutDI->getOperator())) {
36 if (Init->getDef()->getName() !="outs")
37 PrintFatalError(R->getName() + ":invalid def name for output list: use 'outs'");
38 } else
39 PrintFatalError(R->getName() + ":invalid output list: use 'outs'");
40
41 NumDefs = OutDI->getNumArgs();
42
43 DagInit *InDI = R->getValueAsDag("InOperandList");
44 if (DefInit *Init =dyn_cast<DefInit>(InDI->getOperator())) {
45 if (Init->getDef()->getName() !="ins")
46 PrintFatalError(R->getName() + ":invalid def name for input list: use 'ins'");
47 } else
48 PrintFatalError(R->getName() + ":invalid input list: use 'ins'");
49
50 unsigned MIOperandNo = 0;
51 std::set<std::string> OperandNames;
52
for (unsignedi = 0, e = InDI->getNumArgs()+OutDI->getNumArgs(); i != e; ++i){
53 Init *ArgInit;
54 std::string ArgName;
55 if (i < NumDefs) {
56 ArgInit = OutDI->getArg(i);
57 ArgName = OutDI->getArgName(i);
58 } else {
59 ArgInit = InDI->getArg(i-NumDefs);
60 ArgName = InDI->getArgName(i-NumDefs);
61 }
62
63 DefInit *Arg =dyn_cast<DefInit>(ArgInit);
64 if (!Arg)
65 PrintFatalError("Illegal operand forthe '" + R->getName() + "' instruction!");
66
67 Record *Rec = Arg->getDef();
68 std::string PrintMethod ="printOperand";
69 std::string EncoderMethod;
70 std::string OperandType ="OPERAND_UNKNOWN";
71 std::string OperandNamespace ="MCOI";
72 unsigned NumOps = 1;
73 DagInit *MIOpInfo = nullptr;
74 if(Rec->isSubClassOf("RegisterOperand")) {
75 PrintMethod =Rec->getValueAsString("PrintMethod");
76 OperandType =Rec->getValueAsString("OperandType");
77 OperandNamespace =Rec->getValueAsString("OperandNamespace");
78 } else if(Rec->isSubClassOf("Operand")) {
79 PrintMethod =Rec->getValueAsString("PrintMethod");
80 OperandType =Rec->getValueAsString("OperandType");
81
// If thereis an explicit encoder method, use it.
82 EncoderMethod =Rec->getValueAsString("EncoderMethod");
83 MIOpInfo = Rec->getValueAsDag("MIOperandInfo");
84
85
// Verifythat MIOpInfo has an 'ops' root value.
86 if(!isa<DefInit>(MIOpInfo->getOperator()) ||
87 cast<DefInit>(MIOpInfo->getOperator())->getDef()->getName()!= "ops")
88 PrintFatalError("Bad value forMIOperandInfo in operand '" + Rec->getName() +
89 "'\n");
90
91
// If we haveMIOpInfo, then we have #operands equal to number of entries
92
// inMIOperandInfo.
93 if (unsigned NumArgs =MIOpInfo->getNumArgs())
94 NumOps = NumArgs;
95
96 if(Rec->isSubClassOf("PredicateOp"))
97 isPredicable = true;
98 else if(Rec->isSubClassOf("OptionalDefOperand"))
99 hasOptionalDef = true;
100 } else if (Rec->getName() =="variable_ops") {
101 isVariadic = true;
102
continue;
103 } else if(Rec->isSubClassOf("RegisterClass")) {
104 OperandType ="OPERAND_REGISTER";
105 } else if(!Rec->isSubClassOf("PointerLikeRegClass") &&
106 !Rec->isSubClassOf("unknown_class"))
107 PrintFatalError("Unknown operandclass '" + Rec->getName() +
108 "' in '" + R->getName() +"' instruction!");
109
110
// Check thatthe operand has a name and that it's unique.
111 if (ArgName.empty())
112 PrintFatalError("In instruction'" + R->getName() + "', operand #" +
113 Twine(i) + " has noname!");
114 if (!OperandNames.insert(ArgName).second)
115 PrintFatalError("In instruction'" + R->getName() + "', operand #" +
116 Twine(i) + " has thesame name as a previous operand!");
117
118 OperandList.emplace_back(Rec, ArgName,PrintMethod, EncoderMethod,
119 OperandNamespace +"::" + OperandType, MIOperandNo,
120 NumOps, MIOpInfo);
121 MIOperandNo += NumOps;
122 }
123
124
125
// Make sure theconstraints list for each operand is large enough to hold
126
// constraintinfo, even if none is present.
127
for (unsignedi = 0, e = OperandList.size(); i != e; ++i)
128 OperandList[i].Constraints.resize(OperandList[i].MINumOperands);
129 }
上面的Record实例的getValue*方法通过参数指定要访问的成员名,*部分则指明了成员的类型,这些方法将返回对应类型的成员值。
首先,确定Instruction定义中的OutOperandList与InOperandList分别是以out及ins作为操作符的dag。在52行的循环里依次访问OutOperandList与InOperandList的操作数。可以作为输入、输出操作数的类型有:RegisterOperand,Operand,RegisterClass,variable_ops,PointerLikeRegClass。其中部分的TD定义分别是:
144 class
DAGOperand { }
519 def variable_ops;
526 class
PointerLikeRegClass<intKind> {
526 int RegClassKind = Kind;
528 }
构建了CGIOperandList实例后,要解析对这些操作数限定的描述。在CodeGenInstruction构造函数的342行通过ParseConstraints方法进行解析这些描述。
256 static void
ParseConstraints(conststd::string &CStr, CGIOperandList &Ops) {
257 if (CStr.empty())
return;
258
259
conststd::string delims(",");
260 std::string::size_type bidx, eidx;
261
262 bidx = CStr.find_first_not_of(delims);
263
while (bidx!= std::string::npos) {
264 eidx = CStr.find_first_of(delims, bidx);
265 if (eidx == std::string::npos)
266 eidx = CStr.length();
267
268
ParseConstraint(CStr.substr(bidx,eidx - bidx), Ops);
269 bidx = CStr.find_first_not_of(delims,eidx);
270 }
271 }
Constraints字符串可以描述多个限定,它们由字符“,”分割。因此263行的循环找出每个限定描述字符串,并由下面的方法进行分析。
201 static void
ParseConstraint(conststd::string &CStr, CGIOperandList &Ops) {
202
// EARLY_CLOBBER:@early $reg
203 std::string::size_type wpos =CStr.find_first_of(" \t");
204 std::string::size_type start =CStr.find_first_not_of(" \t");
205 std::string Tok = CStr.substr(start, wpos -start);
206 if (Tok == "@earlyclobber") {
207 std::string Name = CStr.substr(wpos+1);
208 wpos = Name.find_first_not_of("\t");
209 if (wpos == std::string::npos)
210 PrintFatalError("Illegal format for@earlyclobber constraint: '" + CStr + "'");
211 Name = Name.substr(wpos);
212 std::pair<unsigned,unsigned> Op =Ops.ParseOperandName(Name, false);
213
214
// Build thestring for the operand
215 if(!Ops[Op.first].Constraints[Op.second].isNone())
216 PrintFatalError("Operand '" +Name + "' cannot have multiple constraints!");
217 Ops[Op.first].Constraints[Op.second] =
218 CGIOperandList::ConstraintInfo::getEarlyClobber();
219
return;
220 }
222
// Only otherconstraint is "TIED_TO" for now.
223 std::string::size_type pos =CStr.find_first_of('=');
224
assert(pos !=std::string::npos && "Unrecognized constraint");
225 start = CStr.find_first_not_of("\t");
226 std::string Name = CStr.substr(start, pos -start);
227
228
// TIED_TO: $src1= $dst
229 wpos = Name.find_first_of(" \t");
230 if (wpos == std::string::npos)
231 PrintFatalError("Illegal format fortied-to constraint: '" + CStr + "'");
232 std::string DestOpName = Name.substr(0,wpos);
233 std::pair<unsigned,unsigned> DestOp =Ops.ParseOperandName(DestOpName,false);
234
235 Name = CStr.substr(pos+1);
236 wpos = Name.find_first_not_of("\t");
237 if (wpos == std::string::npos)
238 PrintFatalError("Illegal format fortied-to constraint: '" + CStr + "'");
239
240 std::string SrcOpName = Name.substr(wpos);
241 std::pair<unsigned,unsigned> SrcOp =Ops.ParseOperandName(SrcOpName, false);
242 if (SrcOp > DestOp) {
243 std::swap(SrcOp, DestOp);
244 std::swap(SrcOpName, DestOpName);
245 }
246
247 unsigned FlatOpNo =Ops.getFlattenedOperandNumber(SrcOp);
248
249 if (!Ops[DestOp.first].Constraints[DestOp.second].isNone())
250 PrintFatalError("Operand '" +DestOpName +
251 "' cannot have multipleconstraints!");
252 Ops[DestOp.first].Constraints[DestOp.second]=
253 CGIOperandList::ConstraintInfo::getTied(FlatOpNo);
254 }
有两种约束形式。一个是@earlyclobber $reg,表示寄存器reg的内容很早就被破坏了。另一种就是所谓的绑定(TIED_TO)约束,诸如$src = $dst的形式。但无论何种约束形式,都要识别出操作数的名字。
156 std::pair<unsigned,unsigned>
157 CGIOperandList::ParseOperandName(const std::string &Op, bool AllowWholeOp) {
158 if (Op.empty() || Op[0] != '$')
159 PrintFatalError(TheDef->getName() +": Illegal operand name: '" + Op + "'");
160
161 std::string OpName = Op.substr(1);
162 std::string SubOpName;
163
164
// Check to seeif this is $foo.bar.
165 std::string::size_type DotIdx =OpName.find_first_of(".");
166 if (DotIdx != std::string::npos) {
167 SubOpName = OpName.substr(DotIdx+1);
168 if (SubOpName.empty())
169 PrintFatalError(TheDef->getName() +": illegal empty suboperand name in '" +Op +"'");
170 OpName = OpName.substr(0, DotIdx);
171 }
172
173 unsigned OpIdx =
getOperandNamed(OpName);
174
175 if (SubOpName.empty()) {
// If no suboperandname was specified:
176
// If one wasneeded, throw.
177 if (OperandList[OpIdx].MINumOperands > 1&& !AllowWholeOp &&
178 SubOpName.empty())
179 PrintFatalError(TheDef->getName() +": Illegal to refer to"
180 " whole operand part of complexoperand '" + Op + "'");
181
182
// Otherwise,return the operand.
183
return std::make_pair(OpIdx, 0U);
184 }
185
186
// Find thesuboperand number involved.
187 DagInit *MIOpInfo = OperandList[OpIdx].MIOperandInfo;
188 if (!MIOpInfo)
189 PrintFatalError(TheDef->getName() +": unknown suboperand name in '" + Op + "'");
190
191
// Find theoperand with the right name.
192
for (unsignedi = 0, e = MIOpInfo->getNumArgs(); i != e; ++i)
193 if (MIOpInfo->getArgName(i) ==SubOpName)
194
return std::make_pair(OpIdx, i);
195
196
// Otherwise,didn't find it!
197 PrintFatalError(TheDef->getName() +": unknown suboperand name in '" + Op + "'");
198
return std::make_pair(0U, 0U);
199 }
操作数名字必须以$开始,这个名字可以是类似于$foo.bar这样的形式。对这样的名字,foo部分称为操作数,bar部分称为子操作数(参考ssmem定义)。操作数必须在输入或输出操作数列表里出现,这个列表已经被保存到OperandList容器里了,因此只需要操作数在容器里的索引。
136 unsigned
CGIOperandList::getOperandNamed(StringRefName) const {
137 unsigned OpIdx;
138 if (hasOperandNamed(Name,OpIdx))
return OpIdx;
139 PrintFatalError("'" +TheDef->getName() +
140 "' does not have anoperand named '$" + Name + "'!");
141 }
146 bool
CGIOperandList::hasOperandNamed(StringRefName, unsigned &OpIdx)
const {
147
assert(!Name.empty()&& "Cannot search for operand with no name!");
148
for (unsignedi = 0, e = OperandList.size(); i != e; ++i)
149 if (OperandList[i].Name == Name) {
150 OpIdx = i;
151
return true;
152 }
153
return false;
154 }
如果有多个子操作数,还必须明确地给出子操作数。操作数的索引与子操作数在操作数中的索引作为一个std::pair返回给ParseConstraint。在215行,Ops是传入的CGIOperandList类型的引用,CGIOperandList重载了[]操作符,返回OperandList容器指定索引处的内容,即OperandInfo实例。同一行的Constraints是OperandInfo中类型为std::vector<ConstraintInfo>的容器,而getEarlyClobber返回一个ConstraintInfo实例来记录这个EarlyClobber约束。:
33
class ConstraintInfo {
34
enum {None, EarlyClobber, Tied } Kind;
35 unsigned OtherTiedOperand;
36
public:
37 ConstraintInfo() : Kind(None) {}
38
39
static ConstraintInfo
getEarlyClobber() {
40 ConstraintInfo I;
41 I.Kind = EarlyClobber;
42 I.OtherTiedOperand = 0;
43
return I;
44 }
45
46
static ConstraintInfo getTied(unsigned Op) {
47 ConstraintInfo I;
48 I.Kind = Tied;
49 I.OtherTiedOperand = Op;
50
return I;
51 }
同样在253行,getTied方法返回记录绑定约束的ConstraintInfo实例。不过,由于可能涉及子操作数,在将索引交给getTied之前,需要一个方式将操作数与子操作数的索引编码。
179 unsigned
getFlattenedOperandNumber(std::pair<unsigned,unsigned>Op)
const {
180
return OperandList[Op.first].MIOperandNo + Op.second;
181 }
这实际上就是LLVM的MachineInstr给操作数编号的方式——把子操作数一字排开。
回到CodeGenInstruction的构造函数。Instruction定义中的DisableEncoding成员列出了不能编码入输出MachineInstr的操作数,需要通过OperandInfo类型为std::vector<bool>的容器DoNotEncode把它们标记出来。
273 void
CGIOperandList::ProcessDisableEncoding(std::stringDisableEncoding) {
274
while (1) {
275 std::pair<StringRef, StringRef> P =getToken(DisableEncoding, " ,\t");
276 std::string OpName = P.first;
277 DisableEncoding = P.second;
278 if (OpName.empty())
break;
279
280
// Figure outwhich operand this is.
281 std::pair<unsigned,unsigned> Op =
ParseOperandName(OpName, false);
282
283
// Mark theoperand as not-to-be encoded.
284 if (Op.second >=OperandList[Op.first].DoNotEncode.size())
285 OperandList[Op.first].DoNotEncode.resize(Op.second+1);
286 OperandList[Op.first].DoNotEncode[Op.second] = true;
287 }
288
289 }
最后,如果指令被弃用,其Instruction定义还必须继承一个特殊类ComplexDeprecationPredicate(Target.td)。
1110 class
ComplexDeprecationPredicate<stringdep> {
1111 string ComplexDeprecationPredicate = dep;
1112 }
参数dep用于指定进行启用指令检测的函数名,比如(ARMInstrInfo.td):
5715 // 'it' blocks in ARM modejust validate the predicates. The IT itself
5716 // is discarded.
5717 def ITasm : ARMAsmPseudo<"it$mask $cc",(ins it_pred:$cc, it_mask:$mask)>,
5718 ComplexDeprecationPredicate<"IT">;
在对应的MCInstrDesc实例里将会援引函数getITDeprecationInfo来进行检启用指令测。因此,这里将这个字符串保存到CodeGenInstruction的成员DeprecatedReason里。
回到CodeGenDAGPatterns::ParseInstructions,3087行的InstInfo.Operands.NumDefs是这条指令的输出操作数的个数(在前面处理的时候,先处理输出操作数,再是输入操作数)。因此,临时容器Results与Operands分别保存输出与输入操作数,而ImpResults则保存隐含的结果。在3098行,因为没有模式,在调用DAGInstruction的构造函数时传入的第一个参数为null。
相关文章推荐
- 算法导论学习笔记(18)——单源最短路径(Dijkstra算法实现)
- Cocos2d-X 学习笔记 18 Cocos2dx 下对sqlite3 的简单封装
- ExtJs学习笔记(18)_ExtJs嵌入FCK示例
- Swift学习笔记系列——(18)造型
- OAF学习笔记-18- Update后页面显示不是最新的数据的解决方法
- HeadFirst 设计模式学习笔记18--中介者(Mediator)模式拾零
- 黑马程序员——Objective-C程序设计(第4版)学习笔记之18-复制对象——黑马 IOS 技术博客
- swift 学习笔记(18)-函数
- Symbian学习笔记(18) - 初探Web Services API 的使用(中)
- 【C++】学习笔记草稿版18(模板)
- iOS学习笔记18—iOS应用本地化
- 每日学习笔记(18)
- Linux学习笔记18——信号1
- java个人学习笔记18(多线程之间通信+等待唤醒机制)
- OpenCV 2 学习笔记(18): 反向投影
- C++学习笔记 18
- caffe学习笔记18-image1000test200数据集分类与检索完整过程
- Ruby学习笔记(18)_冒号用法
- 算法学习笔记18-查找
- spring学习笔记(18)——切面的优先级