1//===-- BenchmarkResult.cpp -------------------------------------*- C++ -*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "BenchmarkResult.h"
10#include "BenchmarkRunner.h"
11#include "Error.h"
12#include "ValidationEvent.h"
13#include "llvm/ADT/STLExtras.h"
14#include "llvm/ADT/ScopeExit.h"
15#include "llvm/ADT/StringRef.h"
16#include "llvm/ADT/bit.h"
17#include "llvm/ObjectYAML/YAML.h"
18#include "llvm/Support/Errc.h"
19#include "llvm/Support/Error.h"
20#include "llvm/Support/FileOutputBuffer.h"
21#include "llvm/Support/FileSystem.h"
22#include "llvm/Support/Format.h"
23#include "llvm/Support/MathExtras.h"
24#include "llvm/Support/WithColor.h"
25#include "llvm/Support/raw_ostream.h"
26
27static constexpr char kIntegerPrefix[] = "i_0x";
28static constexpr char kDoublePrefix[] = "f_";
29static constexpr char kInvalidOperand[] = "INVALID";
30
31namespace llvm {
32
33namespace {
34
35// A mutable struct holding an LLVMState that can be passed through the
36// serialization process to encode/decode registers and instructions.
37struct YamlContext {
38 YamlContext(const exegesis::LLVMState &State)
39 : State(&State), ErrorStream(LastError),
40 OpcodeNameToOpcodeIdx(State.getOpcodeNameToOpcodeIdxMapping()) {}
41
42 void serializeMCInst(const MCInst &MCInst, raw_ostream &OS) {
43 OS << getInstrName(InstrNo: MCInst.getOpcode());
44 for (const auto &Op : MCInst) {
45 OS << ' ';
46 serializeMCOperand(MCOperand: Op, OS);
47 }
48 }
49
50 void deserializeMCInst(StringRef String, MCInst &Value) {
51 SmallVector<StringRef, 16> Pieces;
52 String.split(A&: Pieces, Separator: " ", /* MaxSplit */ -1, /* KeepEmpty */ false);
53 if (Pieces.empty()) {
54 ErrorStream << "Unknown Instruction: '" << String << "'\n";
55 return;
56 }
57 bool ProcessOpcode = true;
58 for (StringRef Piece : Pieces) {
59 if (ProcessOpcode)
60 Value.setOpcode(getInstrOpcode(InstrName: Piece));
61 else
62 Value.addOperand(Op: deserializeMCOperand(String: Piece));
63 ProcessOpcode = false;
64 }
65 }
66
67 std::string &getLastError() { return ErrorStream.str(); }
68
69 // Returns the recorded error to report to the YAML parser from a
70 // ScalarTraits::input. When ContinueOnError is set, recoverable per-entry
71 // errors (e.g. an unknown opcode in a bitrotted sample) are left recorded for
72 // the caller to inspect but are not reported to the parser, which would
73 // otherwise abort parsing of every remaining document.
74 StringRef getInputError() {
75 return ContinueOnError ? StringRef() : StringRef(getLastError());
76 }
77
78 raw_string_ostream &getErrorStream() { return ErrorStream; }
79
80 // When set, ScalarTraits::input swallows recoverable deserialization errors
81 // (see getInputError) so that readYamls can skip the offending entry instead
82 // of aborting the whole file.
83 bool ContinueOnError = false;
84
85 StringRef getRegName(MCRegister Reg) {
86 // Special case: Reg may be invalid. We have to deal with it explicitly.
87 if (!Reg.isValid())
88 return kNoRegister;
89 const StringRef RegName = State->getRegInfo().getName(RegNo: Reg);
90 if (RegName.empty())
91 ErrorStream << "No register with enum value '" << Reg.id() << "'\n";
92 return RegName;
93 }
94
95 std::optional<MCRegister> getRegNo(StringRef RegName) {
96 std::optional<MCRegister> RegisterNumber =
97 State->getRegisterNumberFromName(RegisterName: RegName);
98 if (!RegisterNumber.has_value())
99 ErrorStream << "No register with name '" << RegName << "'\n";
100 return RegisterNumber;
101 }
102
103private:
104 void serializeIntegerOperand(raw_ostream &OS, int64_t Value) {
105 OS << kIntegerPrefix;
106 OS.write_hex(N: bit_cast<uint64_t>(from: Value));
107 }
108
109 bool tryDeserializeIntegerOperand(StringRef String, int64_t &Value) {
110 if (!String.consume_front(Prefix: kIntegerPrefix))
111 return false;
112 return !String.consumeInteger(Radix: 16, Result&: Value);
113 }
114
115 void serializeFPOperand(raw_ostream &OS, double Value) {
116 OS << kDoublePrefix << format(Fmt: "%la", Vals: Value);
117 }
118
119 bool tryDeserializeFPOperand(StringRef String, double &Value) {
120 if (!String.consume_front(Prefix: kDoublePrefix))
121 return false;
122 char *EndPointer = nullptr;
123 Value = strtod(nptr: String.begin(), endptr: &EndPointer);
124 return EndPointer == String.end();
125 }
126
127 void serializeMCOperand(const MCOperand &MCOperand, raw_ostream &OS) {
128 if (MCOperand.isReg()) {
129 OS << getRegName(Reg: MCOperand.getReg());
130 } else if (MCOperand.isImm()) {
131 serializeIntegerOperand(OS, Value: MCOperand.getImm());
132 } else if (MCOperand.isDFPImm()) {
133 serializeFPOperand(OS, Value: bit_cast<double>(from: MCOperand.getDFPImm()));
134 } else {
135 OS << kInvalidOperand;
136 }
137 }
138
139 MCOperand deserializeMCOperand(StringRef String) {
140 assert(!String.empty());
141 int64_t IntValue = 0;
142 double DoubleValue = 0;
143 if (tryDeserializeIntegerOperand(String, Value&: IntValue))
144 return MCOperand::createImm(Val: IntValue);
145 if (tryDeserializeFPOperand(String, Value&: DoubleValue))
146 return MCOperand::createDFPImm(Val: bit_cast<uint64_t>(from: DoubleValue));
147 if (auto RegNo = getRegNo(RegName: String))
148 return MCOperand::createReg(Reg: *RegNo);
149 if (String != kInvalidOperand)
150 ErrorStream << "Unknown Operand: '" << String << "'\n";
151 return {};
152 }
153
154 StringRef getInstrName(unsigned InstrNo) {
155 const StringRef InstrName = State->getInstrInfo().getName(Opcode: InstrNo);
156 if (InstrName.empty())
157 ErrorStream << "No opcode with enum value '" << InstrNo << "'\n";
158 return InstrName;
159 }
160
161 StringRef findNearestOpcodeName(StringRef InstrName) const {
162 unsigned BestDistance = 2;
163 StringRef Nearest;
164
165 for (const auto &Entry : OpcodeNameToOpcodeIdx) {
166 StringRef Candidate = Entry.getFirst();
167
168 size_t CandidateSize = Candidate.size();
169 size_t InstrSize = InstrName.size();
170 size_t AbsDiff = llvm::AbsoluteDifference(X: CandidateSize, Y: InstrSize);
171
172 if (AbsDiff >= BestDistance)
173 continue;
174
175 unsigned Distance =
176 InstrName.edit_distance(Other: Candidate, /*AllowReplacements=*/true,
177 /*MaxEditDistance=*/BestDistance - 1);
178
179 if (Distance < BestDistance) {
180 BestDistance = Distance;
181 Nearest = Candidate;
182 if (BestDistance == 1)
183 break;
184 }
185 }
186
187 return Nearest;
188 }
189
190 unsigned getInstrOpcode(StringRef InstrName) {
191 auto Iter = OpcodeNameToOpcodeIdx.find(Val: InstrName);
192 if (Iter != OpcodeNameToOpcodeIdx.end())
193 return Iter->second;
194
195 ErrorStream << "No opcode with name '" << InstrName << "'";
196 if (StringRef Nearest = findNearestOpcodeName(InstrName); !Nearest.empty())
197 ErrorStream << " - did you mean '" << Nearest << "' ?";
198 ErrorStream << "\n";
199 return 0;
200 }
201
202 const exegesis::LLVMState *State;
203 std::string LastError;
204 raw_string_ostream ErrorStream;
205 const DenseMap<StringRef, unsigned> &OpcodeNameToOpcodeIdx;
206};
207} // namespace
208
209// Defining YAML traits for IO.
210namespace yaml {
211
212static YamlContext &getTypedContext(void *Ctx) {
213 return *reinterpret_cast<YamlContext *>(Ctx);
214}
215
216// std::vector<MCInst> will be rendered as a list.
217template <> struct SequenceElementTraits<MCInst> {
218 static const bool flow = false;
219};
220
221template <> struct ScalarTraits<MCInst> {
222
223 static void output(const MCInst &Value, void *Ctx, raw_ostream &Out) {
224 getTypedContext(Ctx).serializeMCInst(MCInst: Value, OS&: Out);
225 }
226
227 static StringRef input(StringRef Scalar, void *Ctx, MCInst &Value) {
228 YamlContext &Context = getTypedContext(Ctx);
229 Context.deserializeMCInst(String: Scalar, Value);
230 return Context.getInputError();
231 }
232
233 // By default strings are quoted only when necessary.
234 // We force the use of single quotes for uniformity.
235 static QuotingType mustQuote(StringRef) { return QuotingType::Single; }
236
237 static const bool flow = true;
238};
239
240// std::vector<exegesis::Measure> will be rendered as a list.
241template <> struct SequenceElementTraits<exegesis::BenchmarkMeasure> {
242 static const bool flow = false;
243};
244
245template <>
246struct CustomMappingTraits<std::map<exegesis::ValidationEvent, int64_t>> {
247 static void inputOne(IO &Io, StringRef KeyStr,
248 std::map<exegesis::ValidationEvent, int64_t> &VI) {
249 Expected<exegesis::ValidationEvent> Key =
250 exegesis::getValidationEventByName(Name: KeyStr);
251 if (!Key) {
252 Io.setError("Key is not a valid validation event");
253 return;
254 }
255 Io.mapRequired(Key: KeyStr, Val&: VI[*Key]);
256 }
257
258 static void output(IO &Io, std::map<exegesis::ValidationEvent, int64_t> &VI) {
259 for (auto &IndividualVI : VI) {
260 Io.mapRequired(Key: exegesis::getValidationEventName(VE: IndividualVI.first),
261 Val&: IndividualVI.second);
262 }
263 }
264};
265
266// exegesis::Measure is rendererd as a flow instead of a list.
267// e.g. { "key": "the key", "value": 0123 }
268template <> struct MappingTraits<exegesis::BenchmarkMeasure> {
269 static void mapping(IO &Io, exegesis::BenchmarkMeasure &Obj) {
270 Io.mapRequired(Key: "key", Val&: Obj.Key);
271 if (!Io.outputting()) {
272 // For backward compatibility, interpret debug_string as a key.
273 Io.mapOptional(Key: "debug_string", Val&: Obj.Key);
274 }
275 Io.mapRequired(Key: "value", Val&: Obj.PerInstructionValue);
276 Io.mapOptional(Key: "per_snippet_value", Val&: Obj.PerSnippetValue);
277 Io.mapOptional(Key: "validation_counters", Val&: Obj.ValidationCounters);
278 }
279 static const bool flow = true;
280};
281
282template <> struct ScalarEnumerationTraits<exegesis::Benchmark::ModeE> {
283 static void enumeration(IO &Io, exegesis::Benchmark::ModeE &Value) {
284 Io.enumCase(Val&: Value, Str: "", ConstVal: exegesis::Benchmark::Unknown);
285 Io.enumCase(Val&: Value, Str: "latency", ConstVal: exegesis::Benchmark::Latency);
286 Io.enumCase(Val&: Value, Str: "uops", ConstVal: exegesis::Benchmark::Uops);
287 Io.enumCase(Val&: Value, Str: "inverse_throughput",
288 ConstVal: exegesis::Benchmark::InverseThroughput);
289 }
290};
291
292// std::vector<exegesis::RegisterValue> will be rendered as a list.
293template <> struct SequenceElementTraits<exegesis::RegisterValue> {
294 static const bool flow = false;
295};
296
297template <> struct ScalarTraits<exegesis::RegisterValue> {
298 static constexpr unsigned kRadix = 16;
299 static constexpr bool kSigned = false;
300
301 static void output(const exegesis::RegisterValue &RV, void *Ctx,
302 raw_ostream &Out) {
303 YamlContext &Context = getTypedContext(Ctx);
304 Out << Context.getRegName(Reg: RV.Register) << "=0x"
305 << toString(I: RV.Value, Radix: kRadix, Signed: kSigned);
306 }
307
308 static StringRef input(StringRef String, void *Ctx,
309 exegesis::RegisterValue &RV) {
310 SmallVector<StringRef, 2> Pieces;
311 String.split(A&: Pieces, Separator: "=0x", /* MaxSplit */ -1,
312 /* KeepEmpty */ false);
313 YamlContext &Context = getTypedContext(Ctx);
314 std::optional<MCRegister> RegNo;
315 if (Pieces.size() == 2 && (RegNo = Context.getRegNo(RegName: Pieces[0]))) {
316 RV.Register = *RegNo;
317 const unsigned BitsNeeded = APInt::getBitsNeeded(str: Pieces[1], radix: kRadix);
318 RV.Value = APInt(BitsNeeded, Pieces[1], kRadix);
319 } else {
320 Context.getErrorStream()
321 << "Unknown initial register value: '" << String << "'";
322 }
323 return Context.getInputError();
324 }
325
326 static QuotingType mustQuote(StringRef) { return QuotingType::Single; }
327
328 static const bool flow = true;
329};
330
331template <> struct MappingContextTraits<exegesis::BenchmarkKey, YamlContext> {
332 static void mapping(IO &Io, exegesis::BenchmarkKey &Obj,
333 YamlContext &Context) {
334 Io.setContext(&Context);
335 Io.mapRequired(Key: "instructions", Val&: Obj.Instructions);
336 Io.mapOptional(Key: "config", Val&: Obj.Config);
337 Io.mapRequired(Key: "register_initial_values", Val&: Obj.RegisterInitialValues);
338 }
339};
340
341template <> struct MappingContextTraits<exegesis::Benchmark, YamlContext> {
342 struct NormalizedBinary {
343 NormalizedBinary(IO &io) {}
344 NormalizedBinary(IO &, std::vector<uint8_t> &Data) : Binary(Data) {}
345 std::vector<uint8_t> denormalize(IO &) {
346 std::vector<uint8_t> Data;
347 std::string Str;
348 raw_string_ostream OSS(Str);
349 Binary.writeAsBinary(OS&: OSS);
350 Data.assign(first: Str.begin(), last: Str.end());
351 return Data;
352 }
353
354 BinaryRef Binary;
355 };
356
357 static void mapping(IO &Io, exegesis::Benchmark &Obj, YamlContext &Context) {
358 Io.mapRequired(Key: "mode", Val&: Obj.Mode);
359 Io.mapRequired(Key: "key", Val&: Obj.Key, Ctx&: Context);
360 Io.mapRequired(Key: "cpu_name", Val&: Obj.CpuName);
361 Io.mapRequired(Key: "llvm_triple", Val&: Obj.LLVMTriple);
362 // Optionally map num_repetitions and min_instructions to the same
363 // value to preserve backwards compatibility.
364 // TODO(boomanaiden154): Move min_instructions to mapRequired and
365 // remove num_repetitions once num_repetitions is ready to be removed
366 // completely.
367 if (Io.outputting())
368 Io.mapRequired(Key: "min_instructions", Val&: Obj.MinInstructions);
369 else {
370 Io.mapOptional(Key: "num_repetitions", Val&: Obj.MinInstructions);
371 Io.mapOptional(Key: "min_instructions", Val&: Obj.MinInstructions);
372 }
373 Io.mapRequired(Key: "measurements", Val&: Obj.Measurements);
374 Io.mapRequired(Key: "error", Val&: Obj.Error);
375 Io.mapOptional(Key: "info", Val&: Obj.Info);
376 // AssembledSnippet
377 MappingNormalization<NormalizedBinary, std::vector<uint8_t>> BinaryString(
378 Io, Obj.AssembledSnippet);
379 Io.mapOptional(Key: "assembled_snippet", Val&: BinaryString->Binary);
380 }
381};
382
383template <> struct MappingTraits<exegesis::Benchmark::TripleAndCpu> {
384 static void mapping(IO &Io, exegesis::Benchmark::TripleAndCpu &Obj) {
385 assert(!Io.outputting() && "can only read TripleAndCpu");
386 // Read triple.
387 Io.mapRequired(Key: "llvm_triple", Val&: Obj.LLVMTriple);
388 Io.mapRequired(Key: "cpu_name", Val&: Obj.CpuName);
389 // Drop everything else.
390 }
391};
392
393} // namespace yaml
394
395namespace exegesis {
396
397Expected<std::set<Benchmark::TripleAndCpu>>
398Benchmark::readTriplesAndCpusFromYamls(MemoryBufferRef Buffer) {
399 // We're only mapping a field, drop other fields and silence the corresponding
400 // warnings.
401 yaml::Input Yin(Buffer, nullptr, +[](const SMDiagnostic &, void *Context) {});
402 Yin.setAllowUnknownKeys(true);
403 std::set<TripleAndCpu> Result;
404 yaml::EmptyContext Context;
405 while (Yin.setCurrentDocument()) {
406 TripleAndCpu TC;
407 yamlize(io&: Yin, Val&: TC, /*unused*/ true, Ctx&: Context);
408 if (Yin.error())
409 return errorCodeToError(EC: Yin.error());
410 Result.insert(x: TC);
411 Yin.nextDocument();
412 }
413 return Result;
414}
415
416Expected<Benchmark> Benchmark::readYaml(const LLVMState &State,
417 MemoryBufferRef Buffer) {
418 yaml::Input Yin(Buffer);
419 YamlContext Context(State);
420 Benchmark Benchmark;
421 if (Yin.setCurrentDocument())
422 yaml::yamlize(io&: Yin, Val&: Benchmark, /*unused*/ true, Ctx&: Context);
423 if (!Context.getLastError().empty())
424 return make_error<Failure>(Args&: Context.getLastError());
425 return std::move(Benchmark);
426}
427
428Expected<std::vector<Benchmark>> Benchmark::readYamls(const LLVMState &State,
429 MemoryBufferRef Buffer) {
430 yaml::Input Yin(Buffer);
431 YamlContext Context(State);
432 // Recoverable per-entry errors (e.g. unknown opcodes) are recorded rather
433 // than reported to the parser, so a single bad entry can be dropped without
434 // aborting the read of the whole file.
435 Context.ContinueOnError = true;
436 std::vector<Benchmark> Benchmarks;
437 unsigned NumSkippedEntries = 0;
438 while (Yin.setCurrentDocument()) {
439 Benchmarks.emplace_back();
440 yamlize(io&: Yin, Val&: Benchmarks.back(), /*unused*/ true, Ctx&: Context);
441 if (Yin.error())
442 return errorCodeToError(EC: Yin.error());
443 if (!Context.getLastError().empty()) {
444 // Warn about the unparsable entry (e.g. an unknown opcode from a
445 // bitrotted sample), discard it, and continue so that a single bad entry
446 // doesn't abort the read of the whole file.
447 WithColor::warning() << "skipping benchmark entry: "
448 << Context.getLastError();
449 Context.getLastError().clear();
450 Benchmarks.pop_back();
451 ++NumSkippedEntries;
452 }
453 Yin.nextDocument();
454 }
455 if (NumSkippedEntries)
456 WithColor::warning() << "skipped " << NumSkippedEntries
457 << " benchmark entries that could not be parsed\n";
458 return std::move(Benchmarks);
459}
460
461Error Benchmark::writeYamlTo(const LLVMState &State, raw_ostream &OS) {
462 llvm::scope_exit Cleanup([&] { OS.flush(); });
463 yaml::Output Yout(OS, nullptr /*Ctx*/, 200 /*WrapColumn*/);
464 YamlContext Context(State);
465 Yout.beginDocuments();
466 yaml::yamlize(io&: Yout, Val&: *this, /*unused*/ true, Ctx&: Context);
467 if (!Context.getLastError().empty())
468 return make_error<Failure>(Args&: Context.getLastError());
469 Yout.endDocuments();
470 return Error::success();
471}
472
473Error Benchmark::readYamlFrom(const LLVMState &State, StringRef InputContent) {
474 yaml::Input Yin(InputContent);
475 YamlContext Context(State);
476 if (Yin.setCurrentDocument())
477 yaml::yamlize(io&: Yin, Val&: *this, /*unused*/ true, Ctx&: Context);
478 if (!Context.getLastError().empty())
479 return make_error<Failure>(Args&: Context.getLastError());
480 return Error::success();
481}
482
483void PerInstructionStats::push(const BenchmarkMeasure &BM) {
484 if (Key.empty())
485 Key = BM.Key;
486 assert(Key == BM.Key);
487 ++NumValues;
488 SumValues += BM.PerInstructionValue;
489 MaxValue = std::max(a: MaxValue, b: BM.PerInstructionValue);
490 MinValue = std::min(a: MinValue, b: BM.PerInstructionValue);
491}
492
493bool operator==(const BenchmarkMeasure &A, const BenchmarkMeasure &B) {
494 return std::tie(args: A.Key, args: A.PerInstructionValue, args: A.PerSnippetValue) ==
495 std::tie(args: B.Key, args: B.PerInstructionValue, args: B.PerSnippetValue);
496}
497
498} // namespace exegesis
499} // namespace llvm
500