mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-11-15 11:18:33 +00:00
Build on the plan-based emission to make function definitions cover a more representative breadth of the language: class-typed local variables, and local variables declared with destructuring. This requires some complexity to initialize and manage the class type variables, but ultimately should make all of our compile time benchmark data more representative by covering a more realistic breadth of the language surface. Claude's rough sampling of Carbon, Clang, and LLVM source suggests class types range from 30% to 80% of the locals, and structured bindings range from 0.2% to 2%. When synthesizing, we target approximately 50% of declarations as class types and 2% as destructurings as we anticipate destructuring to be of growing popularity and so want to ensure we're measuring any overhead there. Assisted-by: Claude Code and Antigravity with Gemini --------- Co-authored-by: Geoff Romer <gromer@google.com>
478 lines
18 KiB
C++
478 lines
18 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
|
|
//
|
|
// Benchmarks for compiling Carbon and C++ source at varying sizes, across each
|
|
// phase of compilation. Each benchmark runs in one of two modes: `InProcess`,
|
|
// which drives the compiler libraries directly over an in-memory filesystem, or
|
|
// `Subprocess`, which executes the installed compiler binary on real files to
|
|
// capture the end-to-end command-line cost including process startup.
|
|
|
|
#include <benchmark/benchmark.h>
|
|
|
|
#include <algorithm>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <type_traits>
|
|
|
|
#include "common/filesystem.h"
|
|
#include "llvm/ADT/SmallVector.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/Support/FormatVariadic.h"
|
|
#include "llvm/Support/Program.h"
|
|
#include "llvm/Support/VirtualFileSystem.h"
|
|
#include "testing/base/benchmark_helpers.h"
|
|
#include "testing/base/global_exe_path.h"
|
|
#include "toolchain/base/install_paths.h"
|
|
#include "toolchain/base/install_paths_test_helpers.h"
|
|
#include "toolchain/benchmarking/source_gen.h"
|
|
#include "toolchain/driver/clang_runner.h"
|
|
#include "toolchain/driver/driver.h"
|
|
#include "toolchain/testing/compile_helper.h"
|
|
|
|
namespace Carbon::Testing {
|
|
namespace {
|
|
|
|
// A using declaration and abbreviation to keep the benchmark names short.
|
|
using Lang = SourceGen::Language;
|
|
|
|
// An enumerator used to select compilation phases to benchmark.
|
|
enum class Phase : uint8_t {
|
|
Lex,
|
|
Parse,
|
|
Check,
|
|
};
|
|
|
|
// Selects how compilation is driven: in-process via the compiler libraries, or
|
|
// by executing the installed compiler binary as a subprocess.
|
|
enum class Mode : uint8_t {
|
|
InProcess,
|
|
Subprocess,
|
|
};
|
|
|
|
// Returns the `carbon compile --phase=` flag for a given compilation phase.
|
|
static auto PhaseFlag(Phase phase) -> llvm::StringRef {
|
|
switch (phase) {
|
|
case Phase::Lex:
|
|
return "--phase=lex";
|
|
case Phase::Parse:
|
|
return "--phase=parse";
|
|
case Phase::Check:
|
|
return "--phase=check";
|
|
}
|
|
}
|
|
|
|
// Returns the file name for the `index`-th generated source file, using the
|
|
// language-appropriate extension.
|
|
template <Lang L>
|
|
static auto SourceFileName(size_t index) -> std::string {
|
|
return llvm::formatv("file_{0}.{1}", index,
|
|
L == Lang::Carbon ? "carbon" : "cpp")
|
|
.str();
|
|
}
|
|
|
|
// In-process compilation harness.
|
|
//
|
|
// Drives the compiler's driver or `ClangRunner` directly, locating the prelude
|
|
// or system headers and managing an in-memory VFS in which the compilations
|
|
// occur. This measures the compiler as a library, without process startup cost.
|
|
template <Lang L>
|
|
class InProcessCompiler {
|
|
public:
|
|
// File-count bounds tuned for in-process (library) compilation; see
|
|
// `ComputeFileCount`.
|
|
static constexpr int MinFiles = 8;
|
|
static constexpr int MaxFiles = 128;
|
|
|
|
InProcessCompiler()
|
|
: fs_(new llvm::vfs::InMemoryFileSystem),
|
|
installation_(InstallPaths::MakeForBazelRunfiles(GetExePath())),
|
|
gen_(L) {
|
|
if constexpr (L == Lang::Carbon) {
|
|
driver_.emplace(fs_, &installation_, /*input_stream=*/nullptr,
|
|
&llvm::outs(), &llvm::errs());
|
|
AddPreludeFilesToVfs(installation_, fs_);
|
|
} else {
|
|
overlay_fs_ =
|
|
new llvm::vfs::OverlayFileSystem(llvm::vfs::getRealFileSystem());
|
|
overlay_fs_->pushOverlay(fs_);
|
|
runner_.emplace(&installation_, overlay_fs_);
|
|
}
|
|
}
|
|
|
|
// Sets up a set of source files in the VFS for the driver. Each string input
|
|
// is materialized into a virtual file and a list of the virtual filenames is
|
|
// returned.
|
|
auto SetUpFiles(llvm::ArrayRef<std::string> sources)
|
|
-> llvm::SmallVector<std::string> {
|
|
llvm::SmallVector<std::string> file_names;
|
|
file_names.reserve(sources.size());
|
|
for (auto [i, source] : llvm::enumerate(sources)) {
|
|
file_names.push_back(SourceFileName<L>(i));
|
|
fs_->addFile(file_names.back(), /*ModificationTime=*/0,
|
|
llvm::MemoryBuffer::getMemBuffer(source));
|
|
}
|
|
return file_names;
|
|
}
|
|
|
|
auto RunCompile(llvm::StringRef file_name, Phase phase) -> bool {
|
|
if constexpr (L == Lang::Carbon) {
|
|
return driver_
|
|
->RunCommand({"compile", PhaseFlag(phase), "--no-include-carbon-core",
|
|
file_name})
|
|
.success;
|
|
}
|
|
|
|
// We only support check and lex phases for C++ as it doesn't have a
|
|
// meaningful parse phase in Clang.
|
|
switch (phase) {
|
|
case Phase::Check: {
|
|
auto result = runner_->RunWithNoRuntimes({"-fsyntax-only", file_name});
|
|
return result.ok() && *result;
|
|
}
|
|
case Phase::Lex: {
|
|
auto result =
|
|
runner_->RunWithNoRuntimes({"-E", "-o", "/dev/null", file_name});
|
|
return result.ok() && *result;
|
|
}
|
|
case Phase::Parse:
|
|
CARBON_FATAL("No parse phase to benchmark in Clang");
|
|
}
|
|
}
|
|
|
|
auto gen() -> SourceGen& { return gen_; }
|
|
|
|
private:
|
|
llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> fs_;
|
|
llvm::IntrusiveRefCntPtr<llvm::vfs::OverlayFileSystem> overlay_fs_;
|
|
const InstallPaths installation_;
|
|
std::optional<Driver> driver_;
|
|
std::optional<ClangRunner> runner_;
|
|
SourceGen gen_;
|
|
};
|
|
|
|
// Subprocess compilation harness.
|
|
//
|
|
// Materializes generated sources as real files in a temporary directory and
|
|
// compiles them by executing the installed `carbon` or `clang` binary as a
|
|
// subprocess. This measures the full end-to-end cost users see on the command
|
|
// line, including process startup, rather than only the compiler-as-a-library
|
|
// cost measured by `InProcessCompiler`.
|
|
template <Lang L>
|
|
class SubprocessCompiler {
|
|
public:
|
|
// Subprocess launches are expensive, so use fewer files than the in-process
|
|
// harness to keep benchmark time reasonable; see `ComputeFileCount`.
|
|
static constexpr int MinFiles = 4;
|
|
static constexpr int MaxFiles = 64;
|
|
|
|
SubprocessCompiler()
|
|
: installation_(InstallPaths::MakeForBazelRunfiles(GetExePath())),
|
|
gen_(L) {
|
|
carbon_path_ = (installation_.root() / "carbon-busybox").string();
|
|
clang_path_ = installation_.clang_path().string();
|
|
|
|
// Leave stdin connected to the parent, but discard the compiler's stdout
|
|
// and stderr so benchmark output isn't cluttered.
|
|
redirects_[0] = std::nullopt;
|
|
redirects_[1] = llvm::StringRef("/dev/null");
|
|
redirects_[2] = llvm::StringRef("/dev/null");
|
|
}
|
|
|
|
// Sets up a set of source files on the real filesystem. Each string input is
|
|
// written into a file in a temporary directory and a list of the full file
|
|
// paths is returned.
|
|
auto SetUpFiles(llvm::ArrayRef<std::string> sources)
|
|
-> llvm::SmallVector<std::string> {
|
|
auto tmp_dir_result = Filesystem::MakeTmpDir();
|
|
CARBON_CHECK(tmp_dir_result.ok(), "Failed to create temp dir: {0}",
|
|
tmp_dir_result.error());
|
|
tmp_dir_ = std::move(*tmp_dir_result);
|
|
|
|
llvm::SmallVector<std::string> file_names;
|
|
file_names.reserve(sources.size());
|
|
for (auto [i, source] : llvm::enumerate(sources)) {
|
|
std::string file_name = SourceFileName<L>(i);
|
|
auto write_result = tmp_dir_->WriteFileFromString(file_name, source);
|
|
CARBON_CHECK(write_result.ok(), "Failed to write file: {0}",
|
|
write_result.error());
|
|
file_names.push_back((tmp_dir_->path() / file_name).string());
|
|
}
|
|
return file_names;
|
|
}
|
|
|
|
auto RunCompile(llvm::StringRef file_name, Phase phase) -> bool {
|
|
llvm::StringRef program;
|
|
llvm::SmallVector<llvm::StringRef> args;
|
|
|
|
if constexpr (L == Lang::Carbon) {
|
|
program = carbon_path_;
|
|
args = {"carbon", "compile", PhaseFlag(phase), "--no-include-carbon-core",
|
|
file_name};
|
|
} else {
|
|
program = clang_path_;
|
|
args = {"clang++", "--driver-mode=g++"};
|
|
|
|
// We only support check and lex phases for C++ as it doesn't have a
|
|
// meaningful parse phase in Clang.
|
|
switch (phase) {
|
|
case Phase::Check:
|
|
args.push_back("-fsyntax-only");
|
|
break;
|
|
case Phase::Lex:
|
|
args.push_back("-E");
|
|
args.push_back("-o");
|
|
args.push_back("/dev/null");
|
|
break;
|
|
case Phase::Parse:
|
|
CARBON_FATAL("No parse phase to benchmark in Clang");
|
|
}
|
|
args.push_back(file_name);
|
|
}
|
|
|
|
std::string err_msg;
|
|
bool execution_failed = false;
|
|
int exit_code = llvm::sys::ExecuteAndWait(
|
|
program, args, /*Env=*/std::nullopt, redirects_,
|
|
/*SecondsToWait=*/0, /*MemoryLimit=*/0, &err_msg, &execution_failed);
|
|
return exit_code == 0 && !execution_failed;
|
|
}
|
|
|
|
auto gen() -> SourceGen& { return gen_; }
|
|
|
|
private:
|
|
const InstallPaths installation_;
|
|
SourceGen gen_;
|
|
std::string carbon_path_;
|
|
std::string clang_path_;
|
|
std::optional<llvm::StringRef> redirects_[3];
|
|
std::optional<Filesystem::RemovingDir> tmp_dir_;
|
|
};
|
|
|
|
// Selects the compilation harness for a given language and mode.
|
|
template <Lang L, Mode M>
|
|
using CompilerFor =
|
|
std::conditional_t<M == Mode::InProcess, InProcessCompiler<L>,
|
|
SubprocessCompiler<L>>;
|
|
|
|
// Parameters for the dense declarations pattern, which models an API file:
|
|
// classes of mostly function declarations, with a few small inline definitions.
|
|
static auto DenseDeclParams() -> SourceGen::DenseDeclParams {
|
|
SourceGen::DenseDeclParams params;
|
|
params.class_params.inline_function_defs = 1;
|
|
params.class_params.inline_getters = 1;
|
|
params.class_params.inline_forwarders = 1;
|
|
return params;
|
|
}
|
|
|
|
// Parameters for the implementation file pattern: classes whose functions and
|
|
// methods are all defined out-of-line, with the default definition sizes. Two
|
|
// thirds of the functions are methods, and they average two parameters, as in
|
|
// the Carbon toolchain's, LLVM's, and Clang's implementation files.
|
|
static auto ImplFileParams() -> SourceGen::DenseDeclParams {
|
|
SourceGen::DenseDeclParams params;
|
|
params.class_params = {.public_function_decls = 2,
|
|
.public_function_decl_params = {.max_params = 2},
|
|
.public_method_decls = 6,
|
|
.public_method_decl_params = {.max_params = 2},
|
|
.private_function_decls = 2,
|
|
.private_function_decl_params = {.max_params = 2},
|
|
.private_method_decls = 2,
|
|
.private_method_decl_params = {.max_params = 2},
|
|
.private_field_decls = 4};
|
|
params.define_decls_out_of_line = true;
|
|
return params;
|
|
}
|
|
|
|
// Benchmark on multiple files of the same size but with different source code
|
|
// in order to avoid branch prediction perfectly learning a particular file's
|
|
// structure and shape, and to get closer to a cache-cold benchmark number which
|
|
// is what we generally expect to care about in practice. We enforce an upper
|
|
// bound to avoid excessive benchmark time and a lower bound to avoid anchoring
|
|
// on a single source file that may have unrepresentative content.
|
|
//
|
|
// For simplicity, we compute a number of files from the target line count as a
|
|
// heuristic, clamped to the `[min_files, max_files]` range provided by the
|
|
// compilation harness.
|
|
static auto ComputeFileCount(int target_lines, int min_files,
|
|
[[maybe_unused]] int max_files) -> int {
|
|
int file_count = (1024 * 1024) / target_lines;
|
|
#ifndef NDEBUG
|
|
// Use a smaller number of files in debug builds where compiles are slower,
|
|
// capping at the release-mode minimum.
|
|
return std::max(1, std::min(min_files, file_count));
|
|
#else
|
|
return std::max(min_files, std::min(max_files, file_count));
|
|
#endif
|
|
}
|
|
|
|
template <Lang L, Phase P, Mode M = Mode::InProcess>
|
|
static auto RunApiFileBenchmark(benchmark::State& state,
|
|
const SourceGen::DenseDeclParams& params)
|
|
-> void {
|
|
using Compiler = CompilerFor<L, M>;
|
|
Compiler bench;
|
|
CompileHelper carbon_compile_helper;
|
|
|
|
int target_lines = state.range(0);
|
|
int num_files =
|
|
ComputeFileCount(target_lines, Compiler::MinFiles, Compiler::MaxFiles);
|
|
if constexpr (L == Lang::Cpp) {
|
|
// Reduce the number of files with C++ to balance the longer compile times.
|
|
num_files /= 2;
|
|
}
|
|
|
|
llvm::SmallVector<std::string> sources;
|
|
sources.reserve(num_files);
|
|
|
|
double total_bytes = 0.0;
|
|
double total_lines = 0.0;
|
|
double total_tokens = 0.0;
|
|
|
|
ssize_t first_bytes = 0;
|
|
ssize_t first_lines = 0;
|
|
|
|
for (auto _ : llvm::seq(num_files)) {
|
|
sources.push_back(bench.gen().GenApiFileDenseDecls(target_lines, params));
|
|
const auto& source = sources.back();
|
|
ssize_t bytes = source.size();
|
|
ssize_t lines = llvm::count(source, '\n');
|
|
|
|
total_bytes += bytes;
|
|
total_lines += lines;
|
|
if constexpr (L == Lang::Carbon) {
|
|
total_tokens += carbon_compile_helper.GetTokenizedBuffer(source).size();
|
|
}
|
|
|
|
// The generator varies each file's content but not its size, which keeps
|
|
// results comparable across files and runs.
|
|
if (sources.size() == 1) {
|
|
first_bytes = bytes;
|
|
first_lines = lines;
|
|
continue;
|
|
}
|
|
|
|
CARBON_CHECK(bytes == first_bytes,
|
|
"Generated file {0} has {1} bytes but file 0 has {2}.",
|
|
sources.size() - 1, bytes, first_bytes);
|
|
CARBON_CHECK(lines == first_lines,
|
|
"Generated file {0} has {1} lines but file 0 has {2}.",
|
|
sources.size() - 1, lines, first_lines);
|
|
}
|
|
|
|
state.counters["Bytes"] =
|
|
benchmark::Counter(total_bytes / sources.size(),
|
|
benchmark::Counter::kIsIterationInvariantRate);
|
|
state.counters["Lines"] =
|
|
benchmark::Counter(total_lines / sources.size(),
|
|
benchmark::Counter::kIsIterationInvariantRate);
|
|
if constexpr (L == Lang::Carbon) {
|
|
state.counters["Tokens"] =
|
|
benchmark::Counter(total_tokens / sources.size(),
|
|
benchmark::Counter::kIsIterationInvariantRate);
|
|
}
|
|
|
|
// Set up the sources as files for compilation.
|
|
llvm::SmallVector<std::string> file_names = bench.SetUpFiles(sources);
|
|
CARBON_CHECK(static_cast<int>(file_names.size()) == num_files);
|
|
|
|
// We benchmark in batches of files to avoid benchmarking any peculiarities of
|
|
// a single file.
|
|
while (state.KeepRunningBatch(num_files)) {
|
|
for (ssize_t i = 0; i < num_files;) {
|
|
// We block optimizing `i` as that has proven both more effective at
|
|
// blocking the loop from being optimized away and avoiding disruption of
|
|
// the generated code that we're benchmarking.
|
|
DoNotOptimize(i);
|
|
|
|
bool success = bench.RunCompile(file_names[i], P);
|
|
CARBON_CHECK(success, "Compilation failed for file: {0}", file_names[i]);
|
|
|
|
// We use the compilation success to step through the file names,
|
|
// establishing a dependency between each lookup. This doesn't fully allow
|
|
// us to measure latency rather than throughput, but minimizes any skew in
|
|
// measurements from speculating the start of the next compilation.
|
|
i += static_cast<ssize_t>(success);
|
|
}
|
|
}
|
|
}
|
|
|
|
template <Lang L, Phase P, Mode M = Mode::InProcess>
|
|
static auto BM_CompileApiFileDenseDecls(benchmark::State& state) -> void {
|
|
RunApiFileBenchmark<L, P, M>(state, DenseDeclParams());
|
|
}
|
|
|
|
template <Lang L, Phase P, Mode M = Mode::InProcess>
|
|
static auto BM_CompileImplFile(benchmark::State& state) -> void {
|
|
RunApiFileBenchmark<L, P, M>(state, ImplFileParams());
|
|
}
|
|
|
|
// A thin wrapper for the subprocess benchmarks: they reuse the shared
|
|
// implementation above, but register under their own (terser) name rather than
|
|
// spelling out `Mode::Subprocess` at each registration.
|
|
template <Lang L, Phase P>
|
|
static auto BM_CompileExecApiFileDenseDecls(benchmark::State& state) -> void {
|
|
BM_CompileApiFileDenseDecls<L, P, Mode::Subprocess>(state);
|
|
}
|
|
|
|
// Applies the range configuration of the dense declarations benchmarks:
|
|
// 256-line test cases through 256k-line test cases.
|
|
static auto ConfigureCompileBenchmark(benchmark::Benchmark* b) -> void {
|
|
b->RangeMultiplier(4)->Range(256, static_cast<int64_t>(256 * 1024));
|
|
}
|
|
|
|
// The implementation file benchmarks start at 1024 lines instead, because a
|
|
// class with all of its functions defined doesn't fit in 256 lines.
|
|
static auto ConfigureImplFileBenchmark(benchmark::Benchmark* b) -> void {
|
|
b->RangeMultiplier(4)->Range(1024, static_cast<int64_t>(256 * 1024));
|
|
}
|
|
|
|
// In-process benchmarks measure the compiler as a library across each phase of
|
|
// compilation. The mode defaults to `Mode::InProcess`.
|
|
BENCHMARK(BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Lex>)
|
|
->Apply(ConfigureCompileBenchmark);
|
|
BENCHMARK(BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Parse>)
|
|
->Apply(ConfigureCompileBenchmark);
|
|
BENCHMARK(BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Check>)
|
|
->Apply(ConfigureCompileBenchmark);
|
|
BENCHMARK(BM_CompileApiFileDenseDecls<Lang::Cpp, Phase::Lex>)
|
|
->Apply(ConfigureCompileBenchmark);
|
|
BENCHMARK(BM_CompileApiFileDenseDecls<Lang::Cpp, Phase::Check>)
|
|
->Apply(ConfigureCompileBenchmark);
|
|
|
|
// Subprocess benchmarks measure the same work end-to-end by executing the
|
|
// installed compiler binary, including process startup cost. They use real
|
|
// (wall-clock) time because the compilation happens in a child process whose
|
|
// CPU time isn't attributed to the benchmark process.
|
|
BENCHMARK(BM_CompileExecApiFileDenseDecls<Lang::Carbon, Phase::Lex>)
|
|
->Apply(ConfigureCompileBenchmark)
|
|
->UseRealTime();
|
|
BENCHMARK(BM_CompileExecApiFileDenseDecls<Lang::Carbon, Phase::Parse>)
|
|
->Apply(ConfigureCompileBenchmark)
|
|
->UseRealTime();
|
|
BENCHMARK(BM_CompileExecApiFileDenseDecls<Lang::Carbon, Phase::Check>)
|
|
->Apply(ConfigureCompileBenchmark)
|
|
->UseRealTime();
|
|
BENCHMARK(BM_CompileExecApiFileDenseDecls<Lang::Cpp, Phase::Lex>)
|
|
->Apply(ConfigureCompileBenchmark)
|
|
->UseRealTime();
|
|
BENCHMARK(BM_CompileExecApiFileDenseDecls<Lang::Cpp, Phase::Check>)
|
|
->Apply(ConfigureCompileBenchmark)
|
|
->UseRealTime();
|
|
|
|
// In-process benchmarks of the implementation file pattern.
|
|
BENCHMARK(BM_CompileImplFile<Lang::Carbon, Phase::Lex>)
|
|
->Apply(ConfigureImplFileBenchmark);
|
|
BENCHMARK(BM_CompileImplFile<Lang::Carbon, Phase::Parse>)
|
|
->Apply(ConfigureImplFileBenchmark);
|
|
BENCHMARK(BM_CompileImplFile<Lang::Carbon, Phase::Check>)
|
|
->Apply(ConfigureImplFileBenchmark);
|
|
|
|
BENCHMARK(BM_CompileImplFile<Lang::Cpp, Phase::Lex>)
|
|
->Apply(ConfigureImplFileBenchmark);
|
|
BENCHMARK(BM_CompileImplFile<Lang::Cpp, Phase::Check>)
|
|
->Apply(ConfigureImplFileBenchmark);
|
|
|
|
} // namespace
|
|
} // namespace Carbon::Testing
|