3.5.2.3. 指令描述数组
首先看到385行的getInstructionsByEnumValue方法,指令的这个遍历次序很重要。接着,我们看到387行的SequenceToOffsetTable类型的变量InstrNames,这意味着我们将要进行差分编码。391行则告诉我们,要进行差分编码的是指令的名字。
InstrInfoEmitter::run(续)
381 // Emit all of the MCInstrDesc records in their ENUM ordering.
382 //
383 OS << "\nextern const MCInstrDesc " << TargetName << "Insts[] = {\n";
384 const std::vector< const CodeGenInstruction*> &NumberedInstructions =
385 Target. getInstructionsByEnumValue ();
386
387 SequenceToOffsetTable<std::string> InstrNames;
388 unsigned Num = 0;
389 for ( const CodeGenInstruction *Inst : NumberedInstructions) {
390 // Keep a list of the instruction names.
391 InstrNames.(Inst->TheDef->getName());
392 // Emit the record into the table.
393 (*Inst, Num++, InstrInfo, EmittedLists, OperandInfoIDs, OS);
394 }
395 OS << "};\n\n";
注意在393行调用的emitRecord,参数Num是指令的处理序号。记住我们总是以相同的次序遍历指令。参数InstrInfo就是X86InstrInfo(定义在X86.td)的Record对象,不过emitRecord方法并没有用它。参数EmittedLists则是以指令定义中的Uses或Defs的Record对象为键值,给出这些对象输出所在ImplicitList Num 数组的序号(即 Num )。同样参数OpInfo以输出到OperandInfo Num 数组的字符串为键值,给出该OperandInfo Num 数组的序号。
464 void InstrInfoEmitter::emitRecord ( const CodeGenInstruction &Inst, unsigned Num,
465 Record *InstrInfo,
466 std::map<std::vector<Record*>, unsigned> &EmittedLists,
467 const OperandInfoMapTy &OpInfo,
468 raw_ostream &OS) {
469 int MinOperands = 0;
470 if (!Inst.Operands.empty())
471 // Each logical operand can be multiple MI operands.
472 MinOperands = Inst.Operands.back().MIOperandNo +
473 Inst.Operands.back().MINumOperands;
474
475 OS << " { ";
476 OS << Num << ",\t" << MinOperands << ",\t"
477 << Inst.Operands.NumDefs << ",\t"
478 << Inst.TheDef->getValueAsInt("Size") << ",\t"
479 << SchedModels.getSchedClassIdx(Inst) << ",\t0";
480
481 // Emit all of the target independent flags...
482 if (Inst.isPseudo) OS << "|(1ULL<<MCID::Pseudo)";
483 if (Inst.isReturn) OS << "|(1ULL<<MCID::Return)";
484 if (Inst.isBranch) OS << "|(1ULL<<MCID::Branch)";
485 if (Inst.isIndirectBranch) OS << "|(1ULL<<MCID::IndirectBranch)";
486 if (Inst.isCompare) OS << "|(1ULL<<MCID::Compare)";
487 if (Inst.isMoveImm) OS << "|(1ULL<<MCID::MoveImm)";
488 if (Inst.isBitcast) OS << "|(1ULL<<MCID::Bitcast)";
489 if (Inst.isSelect) OS << "|(1ULL<<MCID::Select)";
490 if (Inst.isBarrier) OS << "|(1ULL<<MCID::Barrier)";
491 if (Inst.hasDelaySlot) OS << "|(1ULL<<MCID::DelaySlot)";
492 if (Inst.isCall) OS << "|(1ULL<<MCID::Call)";
493 if (Inst.canFoldAsLoad) OS << "|(1ULL<<MCID::FoldableAsLoad)";
494 if (Inst.mayLoad) OS << "|(1ULL<<MCID::MayLoad)";
495 if (Inst.mayStore) OS << "|(1ULL<<MCID::MayStore)";
496 if (Inst.isPredicable) OS << "|(1ULL<<MCID::Predicable)";
497 if (Inst.isConvertibleToThreeAddress) OS << "|(1ULL<<MCID::ConvertibleTo3Addr)";
498 if (Inst.isCommutable) OS << "|(1ULL<<MCID::Commutable)";
499 if (Inst.isTerminator) OS << "|(1ULL<<MCID::Terminator)";
500 if (Inst.isReMaterializable) OS << "|(1ULL<<MCID::Rematerializable)";
501 if (Inst.isNotDuplicable) OS << "|(1ULL<<MCID::NotDuplicable)";
502 if (Inst.Operands.hasOptionalDef) OS << "|(1ULL<<MCID::HasOptionalDef)";
503 if (Inst.usesCustomInserter) OS << "|(1ULL<<MCID::UsesCustomInserter)";
504 if (Inst.hasPostISelHook) OS << "|(1ULL<<MCID::HasPostISelHook)";
505 if (Inst.Operands.isVariadic)OS << "|(1ULL<<MCID::Variadic)";
506 if (Inst.hasSideEffects) OS << "|(1ULL<<MCID::UnmodeledSideEffects)";
507 if (Inst.isAsCheapAsAMove) OS << "|(1ULL<<MCID::CheapAsAMove)";
508 if (Inst.hasExtraSrcRegAllocReq) OS << "|(1ULL<<MCID::ExtraSrcRegAllocReq)";
509 if (Inst.hasExtraDefRegAllocReq) OS << "|(1ULL<<MCID::ExtraDefRegAllocReq)";
510 if (Inst.isRegSequence) OS << "|(1ULL<<MCID::RegSequence)";
511 if (Inst.isExtractSubreg) OS << "|(1ULL<<MCID::ExtractSubreg)";
512 if (Inst.isInsertSubreg) OS << "|(1ULL<<MCID::InsertSubreg)";
513 if (Inst.isConvergent) OS << "|(1ULL<<MCID::Convergent)";
514
515 // Emit all of the target-specific flags...
516 BitsInit *TSF = Inst.TheDef->getValueAsBitsInit("TSFlags");
517 if (!TSF)
518 PrintFatalError("no TSFlags?");
519 uint64_t Value = 0;
520 for (unsigned i = 0, e = TSF->getNumBits(); i != e; ++i) {
521 if (BitInit *Bit = dyn_cast<BitInit>(TSF->getBit(i)))
522 Value |= uint64_t(Bit->getValue()) << i;
523 else
524 PrintFatalError("Invalid TSFlags bit in " + Inst.TheDef->getName());
525 }
526 OS << ", 0x";
527 OS.write_hex(Value);
528 OS << "ULL, ";
529
530 // Emit the implicit uses and defs lists...
531 std::vector<Record*> UseList = Inst.TheDef->getValueAsListOfDefs("Uses");
532 if (UseList.empty())
533 OS << "nullptr, ";
534 else
535 OS << "ImplicitList" << EmittedLists[UseList] << ", ";
536
537 std::vector<Record*> DefList = Inst.TheDef->getValueAsListOfDefs("Defs");
538 if (DefList.empty())
539 OS << "nullptr, ";
540 else
541 OS << "ImplicitList" << EmittedLists[DefList] << ", ";
542
543 // Emit the operand info.
544 std::vector<std::string> OperandInfo =(Inst);
545 if (OperandInfo.empty())
546 OS << "nullptr";
547 else
548 OS << "OperandInfo" << OpInfo.find(OperandInfo)->second;
549
550 CodeGenTarget &Target = CDP.getTargetInfo();
551 if (Inst.HasComplexDeprecationPredicate)
552 // Emit a function pointer to the complex predicate method.
553 OS << ", -1 "
554 << ",&get" << Inst.DeprecatedReason << "DeprecationInfo";
555 else if (!Inst.DeprecatedReason.empty())
556 // Emit the Subtarget feature.
557 OS << ", " << Target.getInstNamespace() << "::" << Inst.DeprecatedReason
558 << " ,nullptr";
559 else
560 // Instruction isn't deprecated.
561 OS << ", -1 ,nullptr";
562
563 OS << " }, // Inst #" << Num << " = " << Inst.TheDef->getName() << "\n";
564 }
emitRecord要输出的是一个元素类型为MCInstrDesc的数组。显然,MCInstrDesc是MC用于描述指令的类型,它有如下的数据成员:
138 class MCInstrDesc {
139 public :
140 unsigned short Opcode; // The opcode number
141 unsigned short NumOperands; // Num of args (may be more if variable_ops)
142 unsigned char NumDefs; // Num of args that are definitions
143 unsigned char Size; // Number of bytes in encoding.
144 unsigned short SchedClass; // enum identifying instr sched class
145 uint64_t Flags; // Flags identifying machine instr class
146 uint64_t TSFlags; // Target Specific Flag values
147 const uint16_t *ImplicitUses; // Registers implicitly read by this instr
148 const uint16_t *ImplicitDefs; // Registers implicitly defined by this instr
149 const *OpInfo; // 'NumOperands' entries about operands
150 // Subtarget feature that this is deprecated on, if any
151 // -1 implies this is not deprecated by any single feature. It may still be
152 // deprecated due to a "complex" reason, below.
153 int64_t DeprecatedFeature;
在emitRecord的输出里,140行的Opcode是该MCInstrDesc实例在输出数组中的索引号。143行的Size则是该指令的编码大小。144行的SchedClass实际上是指令的调度类型在CodeGenSchedModels的SchedClasses容器里的序号(getSchedClassIdx方法通过InstrClassMap来确定指令的调度类型,这些调度类型都是第一部分类型)。147~149行的ImplicitUses,ImplicitDefs,OpInfo分别在535,541及548行由EmittedLists及OpInfo负责填充。为了使用OpInfo,在544行调用了GetOperandInfo以获取作为键值的字符串。输出的内容的例子:
extern const MCInstrDesc X86Insts[] = {
…
{ 31, 6, 1, 0, 0, 0|(1ULL<<MCID::MayLoad), 0x0ULL, nullptr, nullptr, OperandInfo15, -1 ,nullptr }, // Inst #31 = ACQUIRE_MOV16rm
{ 32, 6, 1, 0, 0, 0|(1ULL<<MCID::MayLoad), 0x0ULL, nullptr, nullptr, OperandInfo16, -1 ,nullptr }, // Inst #32 = ACQUIRE_MOV32rm
{ 33, 6, 1, 0, 0, 0|(1ULL<<MCID::MayLoad), 0x0ULL, nullptr, nullptr, OperandInfo17, -1 ,nullptr }, // Inst #33 = ACQUIRE_MOV64rm
…
{ 12100, 0, 0, 0, 0, 0|(1ULL<<MCID::UnmodeledSideEffects), 0x80004036ULL, nullptr, ImplicitList3, nullptr, -1 ,nullptr }, // Inst #12100 = XTEST
};
注意这个数组与前面在X86名字空间里输出的匿名枚举常量是一一对应的,因为它们以相同的次序遍历指令集。因此140行Opcode的值实际上就是这些匿名枚举常量。
3.5.2.4. 指令名差分表
指令名字里存在大量重复的片段,因此使用差分表可以起到不错的压缩率。下面398~411行输出名为X86InstrNameData的差分表及X86InstrNameIndices的差分索引表。
InstrInfoEmitter::run(续)
397 // Emit the array of instruction names.
398 InstrNames.();
399 OS << "extern const char " << TargetName << "InstrNameData[] = {\n";
400 InstrNames.(OS, printChar);
401 OS << "};\n\n";
402
403 OS << "extern const unsigned " << TargetName <<"InstrNameIndices[] = {";
404 Num = 0;
405 for ( const CodeGenInstruction *Inst : NumberedInstructions) {
406 // Newline every eight entries.
407 if (Num % 8 == 0)
408 OS << "\n ";
409 OS << InstrNames.(Inst->TheDef->getName()) << "U, ";
410 ++Num;
411 }
412
413 OS << "\n};\n\n";
414
415 // MCInstrInfo initialization routine.
416 OS << "static inline void Init" << TargetName
417 << "MCInstrInfo(MCInstrInfo *II) {\n";
418 OS << " II->InitMCInstrInfo(" << TargetName << "Insts, "
419 << TargetName << "InstrNameIndices, " << TargetName << "InstrNameData, "
420 << NumberedInstructions.size() << ");\n}\n\n";
421
422 OS << "} // End llvm namespace \n";
423
424 OS << "#endif // GET_INSTRINFO_MC_DESC\n\n";
425
426 // Create a TargetInstrInfo subclass to hide the MC layer initialization.
427 OS << "\n#ifdef GET_INSTRINFO_HEADER\n";
428 OS << "#undef GET_INSTRINFO_HEADER\n";
429
430 std::string ClassName = TargetName + "GenInstrInfo";
431 OS << "namespace llvm {\n";
432 OS << "struct " << ClassName << " : public TargetInstrInfo {\n"
433 << " explicit " << ClassName
434 << "(int CFSetupOpcode = -1, int CFDestroyOpcode = -1);\n"
435 << " virtual ~" << ClassName << "();\n"
436 << "};\n";
437 OS << "} // End llvm namespace \n";
438
439 OS << "#endif // GET_INSTRINFO_HEADER\n\n";
440
441 OS << "\n#ifdef GET_INSTRINFO_CTOR_DTOR\n";
442 OS << "#undef GET_INSTRINFO_CTOR_DTOR\n";
443
444 OS << "namespace llvm {\n";
445 OS << "extern const MCInstrDesc " << TargetName << "Insts[];\n";
446 OS << "extern const unsigned " << TargetName << "InstrNameIndices[];\n";
447 OS << "extern const char " << TargetName << "InstrNameData[];\n";
448 OS << ClassName << "::" << ClassName
449 << "(int CFSetupOpcode, int CFDestroyOpcode)\n"
450 << " : TargetInstrInfo(CFSetupOpcode, CFDestroyOpcode) {\n"
451 << " InitMCInstrInfo(" << TargetName << "Insts, " << TargetName
452 << "InstrNameIndices, " << TargetName << "InstrNameData, "
453 << NumberedInstructions.size() << ");\n}\n"
454 << ClassName << "::~" << ClassName << "() {}\n";
455 OS << "} // End llvm namespace \n";
456
457 OS << "#endif // GET_INSTRINFO_CTOR_DTOR\n\n";
458
459 emitOperandNameMappings (OS, Target, NumberedInstructions);
460
461 (OS, Target);
462 }
416~422行输出下面的方法:
static inline void InitX86MCInstrInfo (MCInstrInfo *II) {
II->InitMCInstrInfo(X86Insts, X86InstrNameIndices, X86InstrNameData, 12101);
}
MCInstrInfo::InitMCInstrInfo方法是一个很简单的方法,却是LLVM通用处理框架与目标机器之间的一个重要接口。MCInstrInfo定义如下:
24 class MCInstrInfo {
25 const MCInstrDesc *Desc; // Raw array to allow static init'n
26 const unsigned *InstrNameIndices; // Array for name indices in InstrNameData
27 const char *InstrNameData; // Instruction name string pool
28 unsigned NumOpcodes; // Number of entries in the desc array
29
30 public :
31 /// \brief Initialize MCInstrInfo, called by TableGen auto-generated routines.
32 /// *DO NOT USE*.
33 void InitMCInstrInfo( const *D, const unsigned *NI, const char *ND,
34 unsigned NO) {
35 Desc = D;
36 InstrNameIndices = NI;
37 InstrNameData = ND;
38 NumOpcodes = NO;
39 }
40
41 unsigned getNumOpcodes() const { return NumOpcodes; }
42
43 /// \brief Return the machine instruction descriptor that corresponds to the
44 /// specified instruction opcode.
45 const MCInstrDesc &get(unsigned Opcode) const {
46 assert (Opcode < NumOpcodes && "Invalid opcode!");
47 return Desc[Opcode];
48 }
49
50 /// \brief Returns the name for the instructions with the given opcode.
51 const char *getName(unsigned Opcode) const {
52 assert (Opcode < NumOpcodes && "Invalid opcode!");
53 return &InstrNameData[InstrNameIndices[Opcode]];
54 }
55 };
427~457行输出目标机器特定的TargetInstrInfo派生类。
#ifdef GET_INSTRINFO_HEADER
#undef GET_INSTRINFO_HEADER
namespace llvm {
struct X86GenInstrInfo : public TargetInstrInfo {
explicit X86GenInstrInfo(int CFSetupOpcode = -1, int CFDestroyOpcode = -1);
virtual ~X86GenInstrInfo();
};
} // End llvm namespace
#endif // GET_INSTRINFO_HEADER
#ifdef GET_INSTRINFO_CTOR_DTOR
#undef GET_INSTRINFO_CTOR_DTOR
namespace llvm {
extern const MCInstrDesc X86Insts[];
extern const unsigned X86InstrNameIndices[];
extern const char X86InstrNameData[];
X86GenInstrInfo::X86GenInstrInfo (int CFSetupOpcode, int CFDestroyOpcode)
: TargetInstrInfo(CFSetupOpcode, CFDestroyOpcode) {
InitMCInstrInfo(X86Insts, X86InstrNameIndices, X86InstrNameData, 12101);
}
X86GenInstrInfo::~X86GenInstrInfo() {}
} // End llvm namespace
#endif // GET_INSTRINFO_CTOR_DTOR
3.5.3.5. getNamedOperandIdx方法与操作数类型
在459行调用的emitOperandNameMappings来为某些特别的目标机器(X86不在内)实现一个带有优化性质的方法getNamedOperandIdx。
236 void InstrInfoEmitter::emitOperandNameMappings (raw_ostream &OS,
237 const CodeGenTarget &Target,
238 const std::vector< const CodeGenInstruction*> &NumberedInstructions) {
239
240 const std::string &Namespace = Target.getInstNamespace();
241 std::string OpNameNS = "OpName";
242 // Map of operand names to their enumeration value. This will be used to
243 // generate the OpName enum.
244 std::map<std::string, unsigned> Operands;
245 OpNameMapTy OperandMap;
246
247 (NumberedInstructions, Namespace, Operands, OperandMap);
248
249 OS << "#ifdef GET_INSTRINFO_OPERAND_ENUM\n";
250 OS << "#undef GET_INSTRINFO_OPERAND_ENUM\n";
251 OS << "namespace llvm {\n";
252 OS << "namespace " << Namespace << " {\n";
253 OS << "namespace " << OpNameNS << " { \n";
254 OS << "enum {\n";
255 for ( const auto &Op : Operands)
256 OS << " " << Op.first << " = " << Op.second << ",\n";
257
258 OS << "OPERAND_LAST";
259 OS << "\n};\n";
260 OS << "} // End namespace OpName\n";
261 OS << "} // End namespace " << Namespace << "\n";
262 OS << "} // End namespace llvm\n";
263 OS << "#endif //GET_INSTRINFO_OPERAND_ENUM\n";
264
265 OS << "#ifdef GET_INSTRINFO_NAMED_OPS\n";
266 OS << "#undef GET_INSTRINFO_NAMED_OPS\n";
267 OS << "namespace llvm {\n";
268 OS << "namespace " << Namespace << " {\n";
269 OS << "LLVM_READONLY\n";
270 OS << "int16_t getNamedOperandIdx(uint16_t Opcode, uint16_t NamedIdx) {\n";
271 if (!Operands.empty()) {
272 OS << " static const int16_t OperandMap [][" << Operands.size()
273 << "] = {\n";
274 for ( const auto &Entry : OperandMap) {
275 const std::map<unsigned, unsigned> &OpList = Entry.first;
276 OS << "{";
277
278 // Emit a row of the OperandMap table
279 for (unsigned i = 0, e = Operands.size(); i != e; ++i)
280 OS << (OpList.count(i) == 0 ? -1 : (int)OpList.find(i)->second) << ", ";
281
282 OS << "},\n";
283 }
284 OS << "};\n";
285
286 OS << " switch(Opcode) {\n";
287 unsigned TableIndex = 0;
288 for ( const auto &Entry : OperandMap) {
289 for ( const std::string &Name : Entry.second)
290 OS << " case " << Name << ":\n";
291
292 OS << " return OperandMap[" << TableIndex++ << "][NamedIdx];\n";
293 }
294 OS << " default: return -1;\n";
295 OS << " }\n";
296 } else {
297 // There are no operands, so no need to emit anything
298 OS << " return -1;\n";
299 }
300 OS << "}\n";
301 OS << "} // End namespace " << Namespace << "\n";
302 OS << "} // End namespace llvm\n";
303 OS << "#endif //GET_INSTRINFO_NAMED_OPS\n";
304
305 }
类似的,如果存在相当数量的指令操作数,而且相当数量的指令使用同一组操作数,TableGen提供了一个优化的方式。通过将指令定义的UseNamedOperandTable设置为1,TableGen将生成名为getNamedOperandIdx的方法,可以基于一个操作数的名字(实际上是一个枚举常量),查找在一条指令(MachineInstr)里该操作数的索引。目前只有ADMGPU使用了这个功能。
201 void InstrInfoEmitter::initOperandMapData (
202 const std::vector< const CodeGenInstruction *> &NumberedInstructions,
203 const std::string &Namespace,
204 std::map<std::string, unsigned> &Operands,
205 OpNameMapTy &OperandMap) {
206
207 unsigned NumOperands = 0;
208 for ( const CodeGenInstruction *Inst : NumberedInstructions) {
209 if (!Inst->TheDef->getValueAsBit("UseNamedOperandTable"))
210 continue ;
211 std::map<unsigned, unsigned> OpList;
212 for ( const auto &Info : Inst->Operands) {
213 StrUintMapIter I = Operands.find(Info.Name);
214
215 if (I == Operands.end()) {
216 I = Operands.insert(Operands.begin(),
217 std::pair<std::string, unsigned>(Info.Name, NumOperands++));
218 }
219 OpList[I->second] = Info.MIOperandNo;
220 }
221 OperandMap[OpList].push_back(Namespace + "::" + Inst->TheDef->getName());
222 }
223 }
这个机制最重要的数据结构是一个[Instruction定义中UseNamedOperandTable域为1的指令数]✕[所涉及操作数个数]数组。以指令Opcode作为第一维索引,该指令操作数的相关枚举常量作为第二维,对应项给出该操作数在这个指令里的索引号。
initOperandMapData方法就是准备这个数组的内容。而emitOperandNameMappings就是根据initOperandMapData准备的数据生成getNamedOperandIdx方法以实现上述的查表。
在InstrInfoEmitter::run的最后,调用下面的方法输出代表目标机器所有指令操作数类型的枚举常量。这些枚举常量实际上是.td文件里目标机器Operand派生定义按名字 排序 的次序。另外,匿名指令操作数不在考虑之列。
310 void InstrInfoEmitter::emitOperandTypesEnum (raw_ostream &OS,
311 const CodeGenTarget &Target) {
312
313 const std::string &Namespace = Target.getInstNamespace();
314 std::vector<Record *> Operands = Records.getAllDerivedDefinitions("Operand");
315
316 OS << "\n#ifdef GET_INSTRINFO_OPERAND_TYPES_ENUM\n";
317 OS << "#undef GET_INSTRINFO_OPERAND_TYPES_ENUM\n";
318 OS << "namespace llvm {\n";
319 OS << "namespace " << Namespace << " {\n";
320 OS << "namespace OpTypes { \n";
321 OS << "enum OperandType {\n";
322
323 unsigned EnumVal = 0;
324 for ( const Record *Op : Operands) {
325 if (!Op->isAnonymous())
326 OS << " " << Op->getName() << " = " << EnumVal << ",\n";
327 ++EnumVal;
328 }
329
330 OS << " OPERAND_TYPE_LIST_END" << "\n};\n";
331 OS << "} // End namespace OpTypes\n";
332 OS << "} // End namespace " << Namespace << "\n";
333 OS << "} // End namespace llvm\n";
334 OS << "#endif // GET_INSTRINFO_OPERAND_TYPES_ENUM\n";
335 }
对X86目标机器而言,这些操作数类型定义都在X86InstrInfo.td文件里,一共有79个,没有匿名定义。
#ifdef GET_INSTRINFO_OPERAND_TYPES_ENUM
#undef GET_INSTRINFO_OPERAND_TYPES_ENUM
namespace llvm {
namespace X86 {
namespace OpTypes {
enum OperandType {
AVX512ICC = 0,
AVX512RC = 1,
AVXCC = 2,
…
vz64mem = 79,
OPERAND_TYPE_LIST_END
};
} // End namespace OpTypes
} // End namespace X86
} // End namespace llvm
#endif // GET_INSTRINFO_OPERAND_TYPES_ENUM
至此,对X86GenInstrInfo.inc的输出完成。不过,我们并没有看到有关调度器输出。这是因为LLVM是一个“很有规矩的”项目,X86GenInstrInfo.inc只能给出关于X86指令的数据。调度器数据必须输出到X86GenSubtargetInfo.inc文件,这是下一节的目标。
以上所述就是小编给大家介绍的《LLVM学习笔记(39)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【每日笔记】【Go学习笔记】2019-01-04 Codis笔记
- 【每日笔记】【Go学习笔记】2019-01-02 Codis笔记
- 【每日笔记】【Go学习笔记】2019-01-07 Codis笔记
- Golang学习笔记-调度器学习
- Vue学习笔记(二)------axios学习
- 算法/NLP/深度学习/机器学习面试笔记
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java in a Nutshell, 6th Edition
Benjamin J Evans、David Flanagan / O'Reilly Media / 2014-10 / USD 59.99
The latest edition of Java in a Nutshell is designed to help experienced Java programmers get the most out of Java 7 and 8, but it's also a learning path for new developers. Chock full of examples tha......一起来看看 《Java in a Nutshell, 6th Edition》 这本书的介绍吧!