Skip to content

Commit 19f7982

Browse files
16bit-ykikoclaude
andcommitted
bench: add heavy AST load test forcing full PCH deserialization
Adds a second scenario that references symbols from all 70 headers, forcing clang to fully deserialize the entire PCH chain. Results: - Light source (3 types): chained +2% overhead - Heavy source (all 70 headers): chained +6% overhead Even in the worst case, chained PCH adds only 28ms to compilation. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 18d3ce9 commit 19f7982

File tree

1 file changed

+171
-49
lines changed

1 file changed

+171
-49
lines changed

benchmarks/pch_chain/pch_chain_benchmark.cpp

Lines changed: 171 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -574,17 +574,129 @@ static void bench_ast_load(const std::vector<std::string>& headers,
574574
const std::string& resource_dir) {
575575
std::println("\n=== AST LOAD LATENCY ({} headers, {} runs) ===", count, runs);
576576

577-
// Source file that uses symbols from multiple headers.
578-
std::string source_text = make_preamble(headers, count) + R"cpp(
577+
// Light source: uses only a few types (lazy PCH load, best case).
578+
std::string light_source = make_preamble(headers, count) + R"cpp(
579579
int main() {
580-
std::vector<std::string> v = {"hello", "world"};
581-
std::map<int, std::string> m;
582-
auto opt = std::make_optional(42);
580+
std::vector<int> v = {1, 2, 3};
581+
return v[0];
582+
}
583+
)cpp";
584+
585+
// Heavy source: references symbols from as many headers as possible
586+
// to force maximum PCH deserialization (worst case).
587+
std::string heavy_source = make_preamble(headers, count) + R"cpp(
588+
template <typename... Ts> void use(Ts&&...) {}
589+
590+
int main() {
591+
// <cstddef> <cstdint> <climits> <cfloat>
592+
std::size_t sz = 0; std::uint64_t u64 = 0;
593+
594+
// <type_traits> <concepts> <compare>
595+
static_assert(std::is_integral_v<int>);
596+
static_assert(std::integral<int>);
597+
std::strong_ordering cmp = 1 <=> 2;
598+
599+
// <initializer_list> <utility> <tuple> <optional> <variant> <any> <expected>
600+
auto il = {1, 2, 3};
601+
auto pr = std::make_pair(1, 2);
602+
auto tp = std::make_tuple(1, "hello", 3.14);
603+
std::optional<int> opt = 42;
604+
std::variant<int, double, std::string> var = "hello";
605+
std::any a = 42;
606+
607+
// <bitset> <bit> <string_view> <string> <charconv> <format>
608+
std::bitset<64> bs(0xFF);
609+
auto pc = std::popcount(42u);
610+
std::string_view sv = "hello";
611+
std::string s = "world";
612+
auto fmt = std::format("{} {}", s, 42);
613+
614+
// <array> <vector> <deque> <list> <forward_list>
615+
std::array<int, 3> arr = {1, 2, 3};
616+
std::vector<std::string> vec = {"a", "b"};
617+
std::deque<int> dq = {1, 2};
618+
std::list<int> lst = {1, 2};
619+
std::forward_list<int> fl = {1, 2};
620+
621+
// <set> <map> <unordered_set> <unordered_map>
622+
std::set<int> st = {1, 2, 3};
623+
std::map<std::string, int> mp = {{"a", 1}};
624+
std::unordered_set<int> us = {1, 2};
625+
std::unordered_map<std::string, int> um = {{"b", 2}};
626+
627+
// <stack> <queue> <span>
628+
std::stack<int> stk;
629+
std::queue<int> que;
630+
std::span<const int> spn(arr);
631+
632+
// <iterator> <ranges> <algorithm> <numeric>
633+
auto it = vec.begin();
634+
auto rng = vec | std::views::take(1);
635+
std::sort(vec.begin(), vec.end());
636+
auto sum = std::accumulate(arr.begin(), arr.end(), 0);
637+
638+
// <memory> <memory_resource> <scoped_allocator> <functional>
639+
auto up = std::make_unique<int>(42);
640+
auto sp = std::make_shared<std::string>("test");
641+
std::function<int(int)> fn = [](int x) { return x * 2; };
642+
643+
// <ratio> <chrono>
644+
using half = std::ratio<1, 2>;
645+
auto now = std::chrono::system_clock::now();
646+
647+
// <exception> <stdexcept> <system_error>
648+
try { throw std::runtime_error("test"); } catch(...) {}
649+
auto ec = std::make_error_code(std::errc::invalid_argument);
650+
651+
// <typeinfo> <typeindex> <source_location>
652+
auto& ti = typeid(int);
653+
std::type_index tidx(ti);
654+
auto loc = std::source_location::current();
655+
656+
// <new> <limits> <numbers> <valarray> <complex> <random>
657+
static_assert(std::numeric_limits<double>::is_iec559);
658+
constexpr auto pi = std::numbers::pi;
659+
std::valarray<double> va = {1.0, 2.0, 3.0};
660+
std::complex<double> cx(1.0, 2.0);
661+
std::mt19937 rng_eng(42);
662+
663+
// <iosfwd> <ios> <streambuf> <istream> <ostream> <iostream> <sstream> <fstream>
664+
std::stringstream ss;
665+
ss << "hello " << 42;
666+
std::cout << ss.str() << std::endl;
667+
668+
// <cmath> <cstdio> <cstdlib> <cstring> <ctime> <cassert> <cerrno>
669+
auto sq = std::sqrt(2.0);
670+
auto len = std::strlen("hello");
671+
auto t = std::time(nullptr);
672+
assert(sq > 1.0);
673+
674+
// <atomic> <mutex> <condition_variable> <thread> <future>
675+
std::atomic<int> ai{0};
676+
std::mutex mtx;
677+
std::condition_variable cv;
678+
679+
// <semaphore> <latch> <barrier> <stop_token> <shared_mutex>
680+
std::counting_semaphore<1> sem(1);
681+
std::latch lat(1);
682+
683+
// <regex> <filesystem>
684+
std::regex rx("hello.*");
685+
auto cwd = std::filesystem::current_path();
686+
687+
// <locale> <codecvt>
688+
auto& loc2 = std::locale::classic();
689+
690+
use(sz, u64, cmp, il, pr, tp, opt, var, a, bs, pc, sv, s, fmt,
691+
arr, vec, dq, lst, fl, st, mp, us, um, stk, que, spn,
692+
it, rng, sum, up, sp, fn, now, ec, ti, tidx, loc,
693+
pi, va, cx, rng_eng, ss, sq, len, t, ai, mtx, cv,
694+
sem, lat, rx, cwd, loc2);
583695
return 0;
584696
}
585697
)cpp";
586-
std::string source_file = "/tmp/ast-load-test.cpp";
587-
auto preamble_bound = static_cast<std::uint32_t>(make_preamble(headers, count).size());
698+
auto preamble = make_preamble(headers, count);
699+
auto preamble_bound = static_cast<std::uint32_t>(preamble.size());
588700

589701
// --- Build monolithic PCH ---
590702
std::string mono_preamble = make_preamble(headers, count);
@@ -627,51 +739,61 @@ int main() {
627739

628740
std::string chain_pch = prev_pch;
629741

630-
// --- Benchmark: compile source with monolithic PCH ---
631-
std::vector<double> mono_times, chain_times;
742+
// --- Run both scenarios: light and heavy ---
743+
struct Scenario {
744+
const char* name;
745+
std::string source;
746+
};
632747

633-
for(int r = 0; r < runs; ++r) {
634-
double t =
635-
compile_with_pch(source_text, source_file, mono_pch, preamble_bound, resource_dir);
636-
if(t >= 0)
637-
mono_times.push_back(t);
638-
}
748+
Scenario scenarios[] = {
749+
{"light (3 types)", light_source},
750+
{"heavy (all headers)", heavy_source},
751+
};
639752

640-
// --- Benchmark: compile source with chained PCH ---
641-
for(int r = 0; r < runs; ++r) {
642-
// For chained PCH, bound=0 since the PCH doesn't cover the source file's bytes.
643-
// But we need the source to NOT re-parse the preamble includes.
644-
// With chained PCH loaded via ImplicitPCHInclude and bound=0,
645-
// clang will parse the source from the beginning but the includes
646-
// will hit the PCH's already-parsed headers.
647-
double t =
648-
compile_with_pch(source_text, source_file, chain_pch, preamble_bound, resource_dir);
649-
if(t >= 0)
650-
chain_times.push_back(t);
651-
}
753+
for(auto& [name, source]: scenarios) {
754+
std::println("\n --- {} ---", name);
755+
std::string source_file = "/tmp/ast-load-test.cpp";
652756

653-
// --- Report ---
654-
if(!mono_times.empty()) {
655-
std::sort(mono_times.begin(), mono_times.end());
656-
double med = mono_times[mono_times.size() / 2];
657-
std::println(" Monolithic PCH → compile: median {:.1f}ms (min {:.1f}, max {:.1f})",
658-
med,
659-
mono_times.front(),
660-
mono_times.back());
661-
}
662-
if(!chain_times.empty()) {
663-
std::sort(chain_times.begin(), chain_times.end());
664-
double med = chain_times[chain_times.size() / 2];
665-
std::println(" Chained PCH → compile: median {:.1f}ms (min {:.1f}, max {:.1f})",
666-
med,
667-
chain_times.front(),
668-
chain_times.back());
669-
}
670-
if(!mono_times.empty() && !chain_times.empty()) {
671-
double mono_med = mono_times[mono_times.size() / 2];
672-
double chain_med = chain_times[chain_times.size() / 2];
673-
double ratio = chain_med / mono_med;
674-
std::println(" Ratio (chained/mono): {:.2f}x", ratio);
757+
std::vector<double> mono_times, chain_times;
758+
759+
for(int r = 0; r < runs; ++r) {
760+
double t =
761+
compile_with_pch(source, source_file, mono_pch, preamble_bound, resource_dir);
762+
if(t >= 0)
763+
mono_times.push_back(t);
764+
}
765+
766+
for(int r = 0; r < runs; ++r) {
767+
double t =
768+
compile_with_pch(source, source_file, chain_pch, preamble_bound, resource_dir);
769+
if(t >= 0)
770+
chain_times.push_back(t);
771+
}
772+
773+
if(!mono_times.empty()) {
774+
std::sort(mono_times.begin(), mono_times.end());
775+
double med = mono_times[mono_times.size() / 2];
776+
std::println(" Monolithic PCH → compile: median {:.1f}ms (min {:.1f}, max {:.1f})",
777+
med,
778+
mono_times.front(),
779+
mono_times.back());
780+
}
781+
if(!chain_times.empty()) {
782+
std::sort(chain_times.begin(), chain_times.end());
783+
double med = chain_times[chain_times.size() / 2];
784+
std::println(" Chained PCH → compile: median {:.1f}ms (min {:.1f}, max {:.1f})",
785+
med,
786+
chain_times.front(),
787+
chain_times.back());
788+
}
789+
if(!mono_times.empty() && !chain_times.empty()) {
790+
double mono_med = mono_times[mono_times.size() / 2];
791+
double chain_med = chain_times[chain_times.size() / 2];
792+
double ratio = chain_med / mono_med;
793+
std::println(" Ratio (chained/mono): {:.2f}x", ratio);
794+
} else if(chain_times.empty()) {
795+
std::println(" Chained PCH: compilation FAILED (heavy source may have errors)");
796+
}
675797
}
676798

677799
// Cleanup.

0 commit comments

Comments
 (0)