Files
carbon-lang/toolchain/lexer/tokenized_buffer_benchmark.cpp
T
5c2338dbd0 Introduce a full lexer benchmark. (#3121)
Currently this is focused on benchmarking the identifier and token
lexing paths, but should expand in the future to cover other parts of
the lexer.

In order to effectively benchmark tokens, this adds support to the
`token_kind` library to produce a list of all the tokens in Carbon that
the benchmark can use to create random inputs.

For identifiers, the benchmark has support for benchmarking different
distributions of identifier sizes so it is easy to zoom into the
performance specifically of short or long identifiers.

This benchmarking is motivated by profiling overall toolchain
performance and noticing that an unreasonable amount of time is spent in
the lexer. In turn, the identifier lexing was surprisingly hot. I have
performance improvements in the works following this, but wanted to
separately introduce the benchmarking framework as the review focus will
be completely different.

---------

Co-authored-by: Richard Smith <richard@metafoo.co.uk>
Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
2023-08-22 22:55:06 +00:00

160 lines
5.1 KiB
C++

// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include <benchmark/benchmark.h>
#include "absl/random/random.h"
#include "common/check.h"
#include "llvm/ADT/Sequence.h"
#include "llvm/ADT/StringExtras.h"
#include "toolchain/diagnostics/null_diagnostics.h"
#include "toolchain/lexer/token_kind.h"
#include "toolchain/lexer/tokenized_buffer.h"
namespace Carbon::Testing {
namespace {
class LexerBenchHelper {
public:
explicit LexerBenchHelper(llvm::StringRef text)
: source_(MakeSourceBuffer(text)) {}
auto Lex() -> TokenizedBuffer {
DiagnosticConsumer& consumer = NullDiagnosticConsumer();
return TokenizedBuffer::Lex(source_, consumer);
}
auto DiagnoseErrors() -> std::string {
std::string result;
llvm::raw_string_ostream out(result);
StreamDiagnosticConsumer consumer(out);
auto buffer = TokenizedBuffer::Lex(source_, consumer);
consumer.Flush();
CARBON_CHECK(buffer.has_errors())
<< "Asked to diagnose errors but none found!";
return result;
}
private:
auto MakeSourceBuffer(llvm::StringRef text) -> SourceBuffer {
CARBON_CHECK(fs_.addFile(filename_, /*ModificationTime=*/0,
llvm::MemoryBuffer::getMemBuffer(text)));
return std::move(*SourceBuffer::CreateFromFile(fs_, filename_));
}
llvm::vfs::InMemoryFileSystem fs_;
std::string filename_ = "test.carbon";
SourceBuffer source_;
};
// A large value for measurement stability without making benchmarking too slow.
constexpr int NumTokens = 100000;
void BM_ValidKeywords(benchmark::State& state) {
absl::BitGen gen;
std::string source;
llvm::raw_string_ostream os(source);
llvm::ListSeparator sep(" ");
for (int i : llvm::seq(0, NumTokens)) {
static_cast<void>(i);
int token = absl::Uniform<int>(gen, 0, TokenKind::KeywordTokens.size());
os << sep << TokenKind::KeywordTokens[token].fixed_spelling();
}
LexerBenchHelper helper(source);
for (auto _ : state) {
TokenizedBuffer buffer = helper.Lex();
CARBON_CHECK(!buffer.has_errors());
}
state.counters["TokenRate"] = benchmark::Counter(
NumTokens, benchmark::Counter::kIsIterationInvariantRate);
}
BENCHMARK(BM_ValidKeywords);
auto IdentifierStartChars() -> llvm::ArrayRef<char> {
static llvm::SmallVector<char> chars = [] {
llvm::SmallVector<char> chars;
chars.push_back('_');
for (char c : llvm::seq_inclusive('A', 'Z')) {
chars.push_back(c);
}
for (char c : llvm::seq_inclusive('a', 'z')) {
chars.push_back(c);
}
return chars;
}();
return chars;
}
auto IdentifierChars() -> llvm::ArrayRef<char> {
static llvm::SmallVector<char> chars = [] {
llvm::ArrayRef<char> start_chars = IdentifierStartChars();
llvm::SmallVector<char> chars(start_chars.begin(), start_chars.end());
for (char c : llvm::seq_inclusive('0', '9')) {
chars.push_back(c);
}
return chars;
}();
return chars;
}
void BM_ValidIdentifiers(benchmark::State& state) {
int min_length = state.range(0);
int max_length = state.range(1);
absl::BitGen gen;
std::string source;
llvm::raw_string_ostream os(source);
llvm::ListSeparator sep(" ");
for (int i : llvm::seq(0, NumTokens)) {
static_cast<void>(i);
int length = absl::Uniform<int>(absl::IntervalClosedClosed, gen, min_length,
max_length);
os << sep;
int id_start = source.size();
llvm::StringRef id;
do {
// Erase any prior attempts to find an identifier.
source.resize(id_start);
llvm::ArrayRef<char> start_chars = IdentifierStartChars();
os << start_chars[absl::Uniform<int>(gen, 0, start_chars.size())];
llvm::ArrayRef<char> chars = IdentifierChars();
for (int j : llvm::seq(0, length)) {
static_cast<void>(j);
os << chars[absl::Uniform<int>(gen, 0, chars.size())];
}
// Check if we ended up forming an integer type literal or a keyword, and
// try again.
id = llvm::StringRef(source).substr(id_start);
} while (llvm::any_of(
TokenKind::KeywordTokens,
[id](auto token) { return id == token.fixed_spelling(); }) ||
((id.consume_front("i") || id.consume_front("u") ||
id.consume_front("f")) &&
llvm::all_of(id, [](const char c) { return llvm::isDigit(c); })));
}
LexerBenchHelper helper(source);
for (auto _ : state) {
TokenizedBuffer buffer = helper.Lex();
// Ensure that lexing actually occurs for benchmarking and that it doesn't
// hit errors that would skew the benchmark results.
CARBON_CHECK(!buffer.has_errors()) << helper.DiagnoseErrors();
}
state.counters["TokenRate"] = benchmark::Counter(
NumTokens, benchmark::Counter::kIsIterationInvariantRate);
}
BENCHMARK(BM_ValidIdentifiers)
// Benchmark a few ranges of identifier widths to cover different patterns
// that emerge with small, medium, and longer identifiers.
->Args({1, 3})
->Args({3, 5})
->Args({3, 16})
->Args({12, 64});
} // namespace
} // namespace Carbon::Testing