//===-- BenchmarkResult.cpp -------------------------------------*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "BenchmarkResult.h" #include "BenchmarkRunner.h" #include "Error.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/bit.h" #include "llvm/ObjectYAML/YAML.h" #include "llvm/Support/FileOutputBuffer.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Format.h" #include "llvm/Support/raw_ostream.h" static constexpr const char kIntegerPrefix[] = "i_0x"; static constexpr const char kDoublePrefix[] = "f_"; static constexpr const char kInvalidOperand[] = "INVALID"; namespace llvm { namespace { // A mutable struct holding an LLVMState that can be passed through the // serialization process to encode/decode registers and instructions. struct YamlContext { YamlContext(const exegesis::LLVMState &State) : State(&State), ErrorStream(LastError), OpcodeNameToOpcodeIdx(State.getOpcodeNameToOpcodeIdxMapping()), RegNameToRegNo(State.getRegNameToRegNoMapping()) {} void serializeMCInst(const MCInst &MCInst, raw_ostream &OS) { OS << getInstrName(MCInst.getOpcode()); for (const auto &Op : MCInst) { OS << ' '; serializeMCOperand(Op, OS); } } void deserializeMCInst(StringRef String, MCInst &Value) { SmallVector Pieces; String.split(Pieces, " ", /* MaxSplit */ -1, /* KeepEmpty */ false); if (Pieces.empty()) { ErrorStream << "Unknown Instruction: '" << String << "'\n"; return; } bool ProcessOpcode = true; for (StringRef Piece : Pieces) { if (ProcessOpcode) Value.setOpcode(getInstrOpcode(Piece)); else Value.addOperand(deserializeMCOperand(Piece)); ProcessOpcode = false; } } std::string &getLastError() { return ErrorStream.str(); } raw_string_ostream &getErrorStream() { return ErrorStream; } StringRef getRegName(unsigned RegNo) { // Special case: RegNo 0 is NoRegister. We have to deal with it explicitly. if (RegNo == 0) return kNoRegister; const StringRef RegName = State->getRegInfo().getName(RegNo); if (RegName.empty()) ErrorStream << "No register with enum value '" << RegNo << "'\n"; return RegName; } std::optional getRegNo(StringRef RegName) { auto Iter = RegNameToRegNo.find(RegName); if (Iter != RegNameToRegNo.end()) return Iter->second; ErrorStream << "No register with name '" << RegName << "'\n"; return std::nullopt; } private: void serializeIntegerOperand(raw_ostream &OS, int64_t Value) { OS << kIntegerPrefix; OS.write_hex(bit_cast(Value)); } bool tryDeserializeIntegerOperand(StringRef String, int64_t &Value) { if (!String.consume_front(kIntegerPrefix)) return false; return !String.consumeInteger(16, Value); } void serializeFPOperand(raw_ostream &OS, double Value) { OS << kDoublePrefix << format("%la", Value); } bool tryDeserializeFPOperand(StringRef String, double &Value) { if (!String.consume_front(kDoublePrefix)) return false; char *EndPointer = nullptr; Value = strtod(String.begin(), &EndPointer); return EndPointer == String.end(); } void serializeMCOperand(const MCOperand &MCOperand, raw_ostream &OS) { if (MCOperand.isReg()) { OS << getRegName(MCOperand.getReg()); } else if (MCOperand.isImm()) { serializeIntegerOperand(OS, MCOperand.getImm()); } else if (MCOperand.isDFPImm()) { serializeFPOperand(OS, bit_cast(MCOperand.getDFPImm())); } else { OS << kInvalidOperand; } } MCOperand deserializeMCOperand(StringRef String) { assert(!String.empty()); int64_t IntValue = 0; double DoubleValue = 0; if (tryDeserializeIntegerOperand(String, IntValue)) return MCOperand::createImm(IntValue); if (tryDeserializeFPOperand(String, DoubleValue)) return MCOperand::createDFPImm(bit_cast(DoubleValue)); if (auto RegNo = getRegNo(String)) return MCOperand::createReg(*RegNo); if (String != kInvalidOperand) ErrorStream << "Unknown Operand: '" << String << "'\n"; return {}; } StringRef getInstrName(unsigned InstrNo) { const StringRef InstrName = State->getInstrInfo().getName(InstrNo); if (InstrName.empty()) ErrorStream << "No opcode with enum value '" << InstrNo << "'\n"; return InstrName; } unsigned getInstrOpcode(StringRef InstrName) { auto Iter = OpcodeNameToOpcodeIdx.find(InstrName); if (Iter != OpcodeNameToOpcodeIdx.end()) return Iter->second; ErrorStream << "No opcode with name '" << InstrName << "'\n"; return 0; } const exegesis::LLVMState *State; std::string LastError; raw_string_ostream ErrorStream; const DenseMap &OpcodeNameToOpcodeIdx; const DenseMap &RegNameToRegNo; }; } // namespace // Defining YAML traits for IO. namespace yaml { static YamlContext &getTypedContext(void *Ctx) { return *reinterpret_cast(Ctx); } // std::vector will be rendered as a list. template <> struct SequenceElementTraits { static const bool flow = false; }; template <> struct ScalarTraits { static void output(const MCInst &Value, void *Ctx, raw_ostream &Out) { getTypedContext(Ctx).serializeMCInst(Value, Out); } static StringRef input(StringRef Scalar, void *Ctx, MCInst &Value) { YamlContext &Context = getTypedContext(Ctx); Context.deserializeMCInst(Scalar, Value); return Context.getLastError(); } // By default strings are quoted only when necessary. // We force the use of single quotes for uniformity. static QuotingType mustQuote(StringRef) { return QuotingType::Single; } static const bool flow = true; }; // std::vector will be rendered as a list. template <> struct SequenceElementTraits { static const bool flow = false; }; // exegesis::Measure is rendererd as a flow instead of a list. // e.g. { "key": "the key", "value": 0123 } template <> struct MappingTraits { static void mapping(IO &Io, exegesis::BenchmarkMeasure &Obj) { Io.mapRequired("key", Obj.Key); if (!Io.outputting()) { // For backward compatibility, interpret debug_string as a key. Io.mapOptional("debug_string", Obj.Key); } Io.mapRequired("value", Obj.PerInstructionValue); Io.mapOptional("per_snippet_value", Obj.PerSnippetValue); } static const bool flow = true; }; template <> struct ScalarEnumerationTraits { static void enumeration(IO &Io, exegesis::InstructionBenchmark::ModeE &Value) { Io.enumCase(Value, "", exegesis::InstructionBenchmark::Unknown); Io.enumCase(Value, "latency", exegesis::InstructionBenchmark::Latency); Io.enumCase(Value, "uops", exegesis::InstructionBenchmark::Uops); Io.enumCase(Value, "inverse_throughput", exegesis::InstructionBenchmark::InverseThroughput); } }; // std::vector will be rendered as a list. template <> struct SequenceElementTraits { static const bool flow = false; }; template <> struct ScalarTraits { static constexpr const unsigned kRadix = 16; static constexpr const bool kSigned = false; static void output(const exegesis::RegisterValue &RV, void *Ctx, raw_ostream &Out) { YamlContext &Context = getTypedContext(Ctx); Out << Context.getRegName(RV.Register) << "=0x" << toString(RV.Value, kRadix, kSigned); } static StringRef input(StringRef String, void *Ctx, exegesis::RegisterValue &RV) { SmallVector Pieces; String.split(Pieces, "=0x", /* MaxSplit */ -1, /* KeepEmpty */ false); YamlContext &Context = getTypedContext(Ctx); std::optional RegNo; if (Pieces.size() == 2 && (RegNo = Context.getRegNo(Pieces[0]))) { RV.Register = *RegNo; const unsigned BitsNeeded = APInt::getBitsNeeded(Pieces[1], kRadix); RV.Value = APInt(BitsNeeded, Pieces[1], kRadix); } else { Context.getErrorStream() << "Unknown initial register value: '" << String << "'"; } return Context.getLastError(); } static QuotingType mustQuote(StringRef) { return QuotingType::Single; } static const bool flow = true; }; template <> struct MappingContextTraits { static void mapping(IO &Io, exegesis::InstructionBenchmarkKey &Obj, YamlContext &Context) { Io.setContext(&Context); Io.mapRequired("instructions", Obj.Instructions); Io.mapOptional("config", Obj.Config); Io.mapRequired("register_initial_values", Obj.RegisterInitialValues); } }; template <> struct MappingContextTraits { struct NormalizedBinary { NormalizedBinary(IO &io) {} NormalizedBinary(IO &, std::vector &Data) : Binary(Data) {} std::vector denormalize(IO &) { std::vector Data; std::string Str; raw_string_ostream OSS(Str); Binary.writeAsBinary(OSS); OSS.flush(); Data.assign(Str.begin(), Str.end()); return Data; } BinaryRef Binary; }; static void mapping(IO &Io, exegesis::InstructionBenchmark &Obj, YamlContext &Context) { Io.mapRequired("mode", Obj.Mode); Io.mapRequired("key", Obj.Key, Context); Io.mapRequired("cpu_name", Obj.CpuName); Io.mapRequired("llvm_triple", Obj.LLVMTriple); Io.mapRequired("num_repetitions", Obj.NumRepetitions); Io.mapRequired("measurements", Obj.Measurements); Io.mapRequired("error", Obj.Error); Io.mapOptional("info", Obj.Info); // AssembledSnippet MappingNormalization> BinaryString( Io, Obj.AssembledSnippet); Io.mapOptional("assembled_snippet", BinaryString->Binary); } }; template <> struct MappingTraits { static void mapping(IO &Io, exegesis::InstructionBenchmark::TripleAndCpu &Obj) { assert(!Io.outputting() && "can only read TripleAndCpu"); // Read triple. Io.mapRequired("llvm_triple", Obj.LLVMTriple); Io.mapRequired("cpu_name", Obj.CpuName); // Drop everything else. } }; } // namespace yaml namespace exegesis { Expected> InstructionBenchmark::readTriplesAndCpusFromYamls(MemoryBufferRef Buffer) { // We're only mapping a field, drop other fields and silence the corresponding // warnings. yaml::Input Yin( Buffer, nullptr, +[](const SMDiagnostic &, void *Context) {}); Yin.setAllowUnknownKeys(true); std::set Result; yaml::EmptyContext Context; while (Yin.setCurrentDocument()) { TripleAndCpu TC; yamlize(Yin, TC, /*unused*/ true, Context); if (Yin.error()) return errorCodeToError(Yin.error()); Result.insert(TC); Yin.nextDocument(); } return Result; } Expected InstructionBenchmark::readYaml(const LLVMState &State, MemoryBufferRef Buffer) { yaml::Input Yin(Buffer); YamlContext Context(State); InstructionBenchmark Benchmark; if (Yin.setCurrentDocument()) yaml::yamlize(Yin, Benchmark, /*unused*/ true, Context); if (!Context.getLastError().empty()) return make_error(Context.getLastError()); return std::move(Benchmark); } Expected> InstructionBenchmark::readYamls(const LLVMState &State, MemoryBufferRef Buffer) { yaml::Input Yin(Buffer); YamlContext Context(State); std::vector Benchmarks; while (Yin.setCurrentDocument()) { Benchmarks.emplace_back(); yamlize(Yin, Benchmarks.back(), /*unused*/ true, Context); if (Yin.error()) return errorCodeToError(Yin.error()); if (!Context.getLastError().empty()) return make_error(Context.getLastError()); Yin.nextDocument(); } return std::move(Benchmarks); } Error InstructionBenchmark::writeYamlTo(const LLVMState &State, raw_ostream &OS) { auto Cleanup = make_scope_exit([&] { OS.flush(); }); yaml::Output Yout(OS, nullptr /*Ctx*/, 200 /*WrapColumn*/); YamlContext Context(State); Yout.beginDocuments(); yaml::yamlize(Yout, *this, /*unused*/ true, Context); if (!Context.getLastError().empty()) return make_error(Context.getLastError()); Yout.endDocuments(); return Error::success(); } Error InstructionBenchmark::readYamlFrom(const LLVMState &State, StringRef InputContent) { yaml::Input Yin(InputContent); YamlContext Context(State); if (Yin.setCurrentDocument()) yaml::yamlize(Yin, *this, /*unused*/ true, Context); if (!Context.getLastError().empty()) return make_error(Context.getLastError()); return Error::success(); } void PerInstructionStats::push(const BenchmarkMeasure &BM) { if (Key.empty()) Key = BM.Key; assert(Key == BM.Key); ++NumValues; SumValues += BM.PerInstructionValue; MaxValue = std::max(MaxValue, BM.PerInstructionValue); MinValue = std::min(MinValue, BM.PerInstructionValue); } bool operator==(const BenchmarkMeasure &A, const BenchmarkMeasure &B) { return std::tie(A.Key, A.PerInstructionValue, A.PerSnippetValue) == std::tie(B.Key, B.PerInstructionValue, B.PerSnippetValue); } } // namespace exegesis } // namespace llvm