Bucket normalized perf ranges, branches, and address counts onto containing
ELF functions. Functions are selected using grouped profile mass before
expensive DWARF symbolization begins.
afdo_tools/ChangeLog:
* src/autofdo_correlate_bucket.cc: New file.
* src/autofdo_correlate_detail.h: New file.
Signed-off-by: Kugan Vivekanandarajah <[email protected]>
Co-authored-by: Dhruv Chawla <[email protected]>
Co-authored-by: Rahul Shinde <[email protected]>
---
afdo_tools/src/autofdo_correlate_bucket.cc | 476 +++++++++++++++++++++
afdo_tools/src/autofdo_correlate_detail.h | 94 ++++
2 files changed, 570 insertions(+)
create mode 100644 afdo_tools/src/autofdo_correlate_bucket.cc
create mode 100644 afdo_tools/src/autofdo_correlate_detail.h
diff --git a/afdo_tools/src/autofdo_correlate_bucket.cc
b/afdo_tools/src/autofdo_correlate_bucket.cc
new file mode 100644
index 00000000000..b8178346f93
--- /dev/null
+++ b/afdo_tools/src/autofdo_correlate_bucket.cc
@@ -0,0 +1,476 @@
+/* Copyright The GNU Toolchain Authors
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+<http://www.gnu.org/licenses/>. */
+
+#include "autofdo_correlate_detail.h"
+
+#include "autofdo_options.h"
+#include "symbol_util.h"
+
+#include <algorithm>
+#include <cstdio>
+#include <limits>
+#include <thread>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+namespace autofdo {
+namespace correlate_detail {
+namespace {
+
+constexpr uint64_t MIN_SAMPLES_FOR_GCOV_EMIT = 10;
+
+/* Below this many branch records the threads cost more than they save. */
+constexpr size_t MIN_BRANCHES_FOR_PARALLEL_SWEEP = 4096;
+
+using func_map_key = const dwarf_debug::function_metadata *;
+using func_maps_type
+ = std::unordered_map<func_map_key, per_function_profile_maps>;
+using entry_edge_count_by_addr = std::unordered_map<uint64_t, uint64_t>;
+
+struct function_emit_group_key
+{
+ std::string parent_name;
+ std::string file_path;
+ uint64_t fallback_addr = 0;
+
+ bool operator== (const function_emit_group_key &o) const noexcept
+ {
+ return parent_name == o.parent_name && file_path == o.file_path
+ && fallback_addr == o.fallback_addr;
+ }
+};
+
+struct function_emit_group_hash
+{
+ size_t operator() (const function_emit_group_key &key) const noexcept
+ {
+ size_t h = std::hash<std::string>{}(key.parent_name);
+ hash_combine (h, std::hash<std::string>{}(key.file_path));
+ hash_combine (h, std::hash<uint64_t>{}(key.fallback_addr));
+ return h;
+ }
+};
+
+using group_mass_map = std::unordered_map<function_emit_group_key, uint64_t,
+ function_emit_group_hash>;
+
+/* Mass a function group must reach to enter the profile: the configured
+ SAMPLE_THRESHOLD_FRAC of PROFILE's total, rounded up so a group whose
+ mass equals that fraction is kept, and never below MIN_SAMPLES. */
+uint64_t
+gcov_emit_threshold (const perf_data::profile_reader &profile,
+ double sample_threshold_frac)
+{
+ uint64_t total = 0;
+ if (const auto *ranges = profile.range_counts ())
+ {
+ for (const auto &row : *ranges)
+ if (row.end >= row.beg)
+ total += row.count * (row.end - row.beg + 1);
+ }
+ else
+ for (const auto &entry : profile.get_address_frequencies ())
+ total += entry.second;
+
+ uint64_t threshold = 0;
+ if (sample_threshold_frac <= 0.0)
+ threshold = 0;
+ else if (!(sample_threshold_frac < 1.0))
+ threshold = total;
+ else
+ {
+ const long double product
+ = static_cast<long double> (sample_threshold_frac)
+ * static_cast<long double> (total);
+ if (product >= static_cast<long double> (
+ std::numeric_limits<uint64_t>::max ()))
+ threshold = std::numeric_limits<uint64_t>::max ();
+ else
+ {
+ threshold = static_cast<uint64_t> (product);
+ if (static_cast<long double> (threshold) < product
+ && threshold < std::numeric_limits<uint64_t>::max ())
+ ++threshold;
+ }
+ }
+ return std::max (threshold, MIN_SAMPLES_FOR_GCOV_EMIT);
+}
+
+/* Seed TARGET with one PC per line-table span, taken from the addresses it
+ actually sampled when it has range mass and from its whole body if not. */
+void
+collect_target_line_span_anchors (dwarf_debug::dwarf_reader &dw,
+ correlate_target &target)
+{
+ uint64_t min_addr = 0;
+ uint64_t max_addr = 0;
+ if (perf_data::branch_stack_range_bounds
+ (target.maps.range_count_map, min_addr, max_addr))
+ dw.collect_line_span_seed_pcs (
+ min_addr, max_addr + 1, target.line_span_anchor_pcs);
+ else
+ dw.collect_line_span_seed_pcs (
+ target.maps.start_addr, target.maps.end_addr,
+ target.line_span_anchor_pcs);
+}
+
+/* Key grouping FUNC with the other split parts of the same source function,
+ so the emit threshold sees their combined mass. */
+function_emit_group_key
+function_emit_group (const dwarf_debug::function_metadata &func)
+{
+ return {gcc_symbol::symbol_split_part_parent (
+ dwarf_debug::string_value (func.name)),
+ dwarf_debug::string_value (func.file_path),
+ dwarf_debug::string_empty (func.file_path) ? func.start_addr : 0};
+}
+
+/* True when MAPS has a real address range and carries mass this run can use;
+ range mass only counts when the branch stack is in play. */
+bool
+per_function_maps_have_samples (const per_function_profile_maps &maps,
+ bool use_branch_stack)
+{
+ if (maps.end_addr <= maps.start_addr)
+ return false;
+ if (!maps.addr_counts.empty () || !maps.branch_count_map.empty ())
+ return true;
+ return use_branch_stack && !maps.range_count_map.empty ();
+}
+
+/* FUNC's entry in FUNC_MAPS, seeded with its address range when first
+ created. */
+per_function_profile_maps &
+get_or_create_function_maps (func_maps_type &func_maps,
+ const dwarf_debug::function_metadata &func)
+{
+ auto it = func_maps.find (&func);
+ if (it != func_maps.end ())
+ return it->second;
+ auto inserted = func_maps.emplace (&func, per_function_profile_maps ());
+ inserted.first->second.start_addr = func.start_addr;
+ inserted.first->second.end_addr = func.end_addr;
+ return inserted.first->second;
+}
+
+/* Total sample mass in MAPS, counting a range once per address it covers. */
+uint64_t
+per_function_profile_mass (const per_function_profile_maps &maps)
+{
+ if (!maps.range_count_map.empty ())
+ {
+ uint64_t total = 0;
+ for (const auto &entry : maps.range_count_map)
+ {
+ const perf_data::branch_stack_edge &range = entry.first;
+ if (range.second >= range.first)
+ total += entry.second * (range.second - range.first + 1);
+ }
+ return total;
+ }
+ uint64_t total = 0;
+ for (const auto &entry : maps.addr_counts)
+ total += entry.second;
+ return total;
+}
+
+/* Bucket branches [BEGIN, END) onto the function each one leaves from, and
+ tally the edges entering each callee. Safe to run per worker on disjoint
+ slices, since FUNC_MAPS and ENTRY_EDGE_COUNTS are merged afterwards. */
+void
+assign_branch_edges_to_functions (
+ const std::vector<dwarf_debug::function_metadata> &symtab_functions,
+ const std::vector<perf_data::profile_branch> &branches, size_t begin,
+ size_t end, func_maps_type &func_maps,
+ entry_edge_count_by_addr &entry_edge_counts)
+{
+ uint64_t last_from = 0;
+ const dwarf_debug::function_metadata *func = nullptr;
+ for (size_t i = begin; i < end; ++i)
+ {
+ const perf_data::profile_branch &row = branches[i];
+ if (i == begin || row.from != last_from)
+ {
+ last_from = row.from;
+ func = dwarf_debug::function_containing_address (
+ symtab_functions, row.from);
+ }
+ if (func == nullptr)
+ continue;
+ get_or_create_function_maps (func_maps, *func)
+ .branch_count_map[{row.from, row.to}]
+ += row.count;
+ /* TODO: any edge to a function entry counts as a call, so a backedge to
+ this function's own entry is counted as a self-call. Telling the two
+ apart needs branch-type metadata from the sample. */
+ const dwarf_debug::function_metadata *callee
+ = dwarf_debug::function_at_entry_pc (symtab_functions, row.to);
+ if (callee != nullptr)
+ entry_edge_counts[callee->start_addr] += row.count;
+ }
+}
+
+/* Add COUNT to every function overlapping [BEGIN, END], splitting the range
+ at function boundaries and skipping the gaps between them. The scan
+ position lives in SYMTAB_SWEEP_INDEX, so ranges must arrive in order. */
+void
+assign_range_to_functions (
+ const std::vector<dwarf_debug::function_metadata> &symtab, uint64_t begin,
+ uint64_t end, uint64_t count, func_maps_type &func_maps,
+ size_t &symtab_sweep_index)
+{
+ uint64_t cursor = begin;
+ while (cursor <= end)
+ {
+ const dwarf_debug::function_metadata *func
+ = dwarf_debug::function_containing_address (symtab, cursor);
+ if (func != nullptr)
+ symtab_sweep_index = static_cast<size_t> (func - symtab.data ());
+ if (func == nullptr)
+ {
+ size_t next = symtab_sweep_index;
+ while (next < symtab.size ()
+ && symtab[next].start_addr <= cursor)
+ ++next;
+ if (next == symtab.size () || symtab[next].start_addr > end)
+ return;
+ symtab_sweep_index = next;
+ cursor = symtab[next].start_addr;
+ continue;
+ }
+ uint64_t segment_end = std::min (end, func->end_addr - 1);
+ size_t next = static_cast<size_t> (func - symtab.data ()) + 1;
+ while (next < symtab.size ()
+ && symtab[next].start_addr <= cursor)
+ ++next;
+ if (next < symtab.size ()
+ && symtab[next].start_addr <= segment_end)
+ segment_end = symtab[next].start_addr - 1;
+ get_or_create_function_maps (func_maps, *func)
+ .range_count_map[{cursor, segment_end}]
+ += count;
+ cursor = segment_end + 1;
+ }
+}
+
+/* CONFIGURED workers, capped at TASK_LIMIT so idle threads are not spawned,
+ and never fewer than one. */
+unsigned
+bounded_worker_count (unsigned configured, size_t task_limit)
+{
+ unsigned count = configured == 0 ? DEFAULT_CORRELATE_THREADS : configured;
+ if (task_limit > 0 && task_limit < count)
+ count = static_cast<unsigned> (task_limit);
+ return std::max (1u, count);
+}
+
+} /* anonymous namespace. */
+
+size_t
+estimate_correlate_target_work (const correlate_target &target,
+ unsigned addr_step)
+{
+ size_t work = target.maps.branch_count_map.size ()
+ + target.maps.addr_counts.size ()
+ + target.line_span_anchor_pcs.size ();
+ for (const auto &entry : target.maps.range_count_map)
+ {
+ const perf_data::branch_stack_edge &range = entry.first;
+ if (entry.second != 0 && range.second >= range.first)
+ work
+ += static_cast<size_t> ((range.second - range.first) / addr_step + 1);
+ }
+ return work;
+}
+
+/* Give every target a profile and builder, and seed its line-span anchors.
+ Runs single-threaded before the workers start. */
+void
+materialize_correlate_targets (correlate_context &ctx, profile_builder
&profile)
+{
+ const std::vector<dwarf_debug::function_metadata> &symtab
+ = ctx.reader.symtab_functions ();
+ profile.reserve_profiles (ctx.targets.size ());
+ ctx.emitted_hot_symbols.reserve (ctx.targets.size ());
+ for (correlate_target &target : ctx.targets)
+ {
+ ctx.emitted_hot_symbols.insert (
+ {target.func_name, target.maps.start_addr});
+ target.func_profile = &profile.get_or_create_function_profile (
+ dwarf_debug::string_value (target.func_name), target.maps.start_addr);
+ profile.init_function_builder (
+ *target.func_profile, symtab[target.symtab_index]);
+ target.func_profile->builder->add_incoming_entry_count (
+ target.maps.incoming_entry_count);
+ collect_target_line_span_anchors (ctx.reader, target);
+ }
+}
+
+/* Bucket all of PERF's mass onto symtab functions and keep, as CTX.targets,
+ those whose emit group clears the threshold. False when nothing survives,
+ which leaves no profile worth writing. */
+bool
+bucket_and_select_targets (correlate_context &ctx,
+ const perf_data::profile_reader &perf,
+ const profiler_run_options &run_opts)
+{
+ const std::vector<dwarf_debug::function_metadata> &symtab
+ = ctx.reader.symtab_functions ();
+ if (symtab.empty ())
+ {
+ fprintf (stderr,
+ _("Error: correlate failed: no ELF symtab functions\n"));
+ return false;
+ }
+
+ const uint64_t emit_threshold
+ = gcov_emit_threshold (perf, run_opts.sample_threshold_frac);
+
+ /* Range counts carry the branch-stack mass; without them the profile falls
+ back to plain address frequencies below. */
+ const auto *ranges
+ = run_opts.use_branch_stack ? perf.range_counts () : nullptr;
+ func_maps_type func_maps;
+ if (ranges != nullptr)
+ {
+ size_t sweep_index = 0;
+ for (const perf_data::profile_range &row : *ranges)
+ assign_range_to_functions
+ (symtab, row.beg, row.end, row.count, func_maps, sweep_index);
+ }
+
+ entry_edge_count_by_addr entry_edge_counts;
+ const auto *branches = perf.branch_counts ();
+ if (branches != nullptr && !branches->empty ())
+ {
+ const size_t count = branches->size ();
+ const unsigned workers
+ = bounded_worker_count (run_opts.correlate_threads, count);
+ if (workers <= 1 || count < MIN_BRANCHES_FOR_PARALLEL_SWEEP)
+ assign_branch_edges_to_functions (
+ symtab, *branches, 0, count, func_maps, entry_edge_counts);
+ else
+ {
+ const size_t chunk = (count + workers - 1) / workers;
+ std::vector<func_maps_type> partials (workers);
+ std::vector<entry_edge_count_by_addr> edge_partials (workers);
+ std::vector<std::thread> threads;
+ threads.reserve (workers);
+ for (size_t i = 0; i < workers; ++i)
+ {
+ const size_t begin = i * chunk;
+ if (begin >= count)
+ break;
+ const size_t end = std::min (count, begin + chunk);
+ threads.emplace_back (
+ [&symtab, branches, &partials, &edge_partials, i, begin, end] {
+ assign_branch_edges_to_functions (symtab,
+ *branches,
+ begin,
+ end,
+ partials[i],
+ edge_partials[i]);
+ });
+ }
+ for (std::thread &thread : threads)
+ thread.join ();
+ for (const func_maps_type &part : partials)
+ for (const auto &entry : part)
+ {
+ perf_data::branch_stack_edge_count_map &dst
+ = get_or_create_function_maps (func_maps, *entry.first)
+ .branch_count_map;
+ for (const auto &edge : entry.second.branch_count_map)
+ dst[edge.first] += edge.second;
+ }
+ for (const entry_edge_count_by_addr &part : edge_partials)
+ for (const auto &entry : part)
+ entry_edge_counts[entry.first] += entry.second;
+ }
+ }
+
+ if (ranges == nullptr)
+ for (const auto &entry : perf.get_address_frequencies ())
+ {
+ const dwarf_debug::function_metadata *func
+ = dwarf_debug::function_containing_address (symtab, entry.first);
+ if (func != nullptr)
+ get_or_create_function_maps (func_maps, *func)
+ .addr_counts[entry.first]
+ += entry.second;
+ }
+
+ if (!perf.has_sample_mass ())
+ {
+ fprintf (stderr,
+ _("Error: correlate failed: perf profile has no sample mass\n"));
+ return false;
+ }
+
+ std::unordered_map<func_map_key, function_emit_group_key> group_by_func;
+ group_mass_map group_mass;
+ group_by_func.reserve (func_maps.size ());
+ group_mass.reserve (func_maps.size ());
+ for (const auto &entry : func_maps)
+ {
+ const function_emit_group_key key = function_emit_group (*entry.first);
+ group_by_func.emplace (entry.first, key);
+ group_mass[key] += per_function_profile_mass (entry.second);
+ }
+
+ std::unordered_set<function_emit_group_key, function_emit_group_hash>
+ emitted_groups;
+ for (const auto &entry : group_mass)
+ if (entry.second >= emit_threshold)
+ emitted_groups.insert (entry.first);
+
+ ctx.targets.reserve (func_maps.size ());
+ for (auto &entry : func_maps)
+ {
+ const dwarf_debug::function_metadata *func = entry.first;
+ per_function_profile_maps &maps = entry.second;
+ if (!per_function_maps_have_samples (maps, run_opts.use_branch_stack)
+ || emitted_groups.count (group_by_func.find (func)->second) == 0)
+ continue;
+ const size_t symtab_index = static_cast<size_t> (func - symtab.data ());
+ const auto edge = entry_edge_counts.find (func->start_addr);
+ if (edge != entry_edge_counts.end ())
+ maps.incoming_entry_count = edge->second;
+ correlate_target target;
+ target.func_name = func->name;
+ target.symtab_index = symtab_index;
+ target.maps = std::move (maps);
+ ctx.targets.push_back (std::move (target));
+ }
+
+ if (ctx.targets.empty ())
+ {
+ fprintf (stderr,
+ _("Error: correlate failed: no functions above "
+ "emit threshold\n"));
+ return false;
+ }
+ ctx.num_threads
+ = bounded_worker_count (run_opts.correlate_threads, ctx.targets.size ());
+ return true;
+}
+
+} /* namespace correlate_detail. */
+} /* namespace autofdo. */
diff --git a/afdo_tools/src/autofdo_correlate_detail.h
b/afdo_tools/src/autofdo_correlate_detail.h
new file mode 100644
index 00000000000..4f20f7c1f3a
--- /dev/null
+++ b/afdo_tools/src/autofdo_correlate_detail.h
@@ -0,0 +1,94 @@
+/* Copyright The GNU Toolchain Authors
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+<http://www.gnu.org/licenses/>. */
+
+#ifndef GCC_AUTOFDO_CORRELATE_DETAIL_H
+#define GCC_AUTOFDO_CORRELATE_DETAIL_H
+
+#include "autofdo_model.h"
+#include "dwarf_reader.h"
+#include "profile_reader.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace autofdo {
+
+struct profiler_run_options;
+
+namespace correlate_detail {
+
+constexpr size_t NO_SYMTAB_INDEX = std::numeric_limits<size_t>::max ();
+
+using hot_symbol_set
+ = std::unordered_set<dwarf_debug::function_instance_key,
+ dwarf_debug::function_instance_key_hash>;
+
+/* Raw perf mass bucketed to one function, before any symbolization. */
+struct per_function_profile_maps
+{
+ uint64_t start_addr = 0;
+ uint64_t end_addr = 0;
+ uint64_t incoming_entry_count = 0;
+ perf_data::branch_stack_edge_count_map range_count_map;
+ perf_data::branch_stack_edge_count_map branch_count_map;
+ std::unordered_map<uint64_t, uint64_t> addr_counts;
+};
+
+/* One function selected for the profile, and everything a worker needs to
+ fill it in. */
+struct correlate_target
+{
+ dwarf_debug::interned_string func_name = nullptr;
+ size_t symtab_index = NO_SYMTAB_INDEX;
+ per_function_profile_maps maps;
+ std::vector<uint64_t> line_span_anchor_pcs;
+ size_t estimated_work = 0;
+ function_profile *func_profile = nullptr;
+};
+
+/* State every correlate worker shares; const once the workers start. */
+struct correlate_context
+{
+ dwarf_debug::dwarf_reader &reader;
+ std::vector<correlate_target> targets;
+ hot_symbol_set emitted_hot_symbols;
+ unsigned num_threads = 0;
+ unsigned range_mass_addr_step = 1;
+
+ explicit correlate_context (dwarf_debug::dwarf_reader &dw)
+ : reader (dw)
+ {
+ }
+};
+
+bool bucket_and_select_targets (correlate_context &,
+ const perf_data::profile_reader &,
+ const profiler_run_options &);
+void materialize_correlate_targets (correlate_context &, profile_builder &);
+size_t estimate_correlate_target_work (const correlate_target &, unsigned);
+size_t run_correlate_target_workers (correlate_context &,
+ dwarf_debug::correlate_symbolize_map *);
+
+} /* namespace correlate_detail. */
+} /* namespace autofdo. */
+
+#endif /* GCC_AUTOFDO_CORRELATE_DETAIL_H. */
--
2.34.1