如何在编译时扩展性能分析用的字符串查找表(LUT)
我希望优化自研的性能分析器以降低性能开销。当前实现中,我使用接收字符串参数的ProfilerHelper,将其置于待分析函数f()中:构造函数启动计时,析构函数结束计时并将时间差记录在以该字符串为键的unordered_map中。
现在我想将其改造为更高效的实现:首先在编译时生成包含函数f()名称的字符串查找表(LUT),并将unordered_map替换为与该LUT配对的普通vector。
目前我已用std::string_view实现了基础LUT,但无法找到编译时扩展它的方法。我的初步尝试代码如下:
template<unsigned N> constexpr auto LUT() { std::array<std::string_view, N> Strs{}; for (unsigned n = 0; n < N; n++) { Strs[n] = ""; } return Strs; }; constexpr std::array<std::string_view, 0> StringsLUT { LUT<0>() }; constexpr auto AddString(std::string_view const& Str) { constexpr auto Size = StringsLUT.size(); std::array<std::string_view, Size + 1> Copy{}; for (auto i = 0; i < Size; ++i) Copy[i] = StringsLUT[i]; Copy[Size] = Str; return Copy; }; int main() { constexpr auto Strs = AddString(__builtin_FUNCTION()); //for (auto const Str : Strs) std::cout << Strs[0] << std::endl; }
我的设想是在每个待分析函数f()中调用AddString,以在编译时扩展这个列表,但需要每次用返回的Copy替换StringsLUT,最终得到包含所有函数名的StringsLUT。请问是否有办法在编译时实现这一需求?我刚接触constexpr在LUT中的应用,感谢您的支持。
你的核心问题在于:constexpr全局变量是编译时固定的,无法通过函数调用动态扩展——每个AddString调用只会生成一个新的数组,而无法修改原有的StringsLUT。要实现编译时收集所有待分析函数名并生成可索引的LUT,需要换用模板元编程+静态注册的思路,以下是可行的实现方案:
核心思路
- 为每个待分析函数分配一个唯一的编译时索引。
- 利用静态变量的初始化机制,自动将函数名注册到全局LUT中。
- 用
vector存储计时数据,通过索引直接访问,完全替代unordered_map的哈希查找。
代码实现
1. 全局LUT与索引生成器
#include <array> #include <string_view> #include <vector> #include <atomic> #include <iostream> #include <source_location> // 全局LUT,存储所有待分析函数的名称 inline std::vector<std::string_view> g_profiler_lut; // 原子计数器,用于生成唯一索引(线程安全) inline std::atomic_size_t g_profiler_index_counter = 0; // 模板函数:为每个函数生成唯一索引,并注册函数名到LUT template<typename Func> constexpr size_t get_profiler_index(Func&&, const std::source_location& loc = std::source_location::current()) { // 静态变量:每个函数实例化时只会初始化一次 static const size_t index = []() { // 将函数名插入LUT g_profiler_lut.emplace_back(loc.function_name()); // 返回当前计数器值,然后自增 return g_profiler_index_counter++; }(); return index; }
2. 改造后的ProfilerHelper
#include <chrono> #include <thread> class ProfilerHelper { public: // 接收函数的索引(编译时确定) explicit ProfilerHelper(size_t func_index) : m_func_index(func_index), m_start(std::chrono::steady_clock::now()) {} ~ProfilerHelper() { auto end = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - m_start).count(); // 确保计时数据vector的大小足够 static std::vector<unsigned long long> g_timing_data; if (m_func_index >= g_timing_data.size()) { g_timing_data.resize(m_func_index + 1, 0); } // 累加计时(线程安全可加锁,此处简化处理) g_timing_data[m_func_index] += duration; } private: size_t m_func_index; std::chrono::steady_clock::time_point m_start; }; // 方便使用的宏,自动获取当前函数的索引 #define PROFILE_FUNCTION() \ ProfilerHelper __profiler(get_profiler_index([](){}))
3. 使用示例
void foo() { PROFILE_FUNCTION(); // 模拟函数逻辑 std::this_thread::sleep_for(std::chrono::microseconds(100)); } void bar() { PROFILE_FUNCTION(); // 模拟函数逻辑 std::this_thread::sleep_for(std::chrono::microseconds(200)); } int main() { foo(); bar(); foo(); // 输出统计结果 static auto& timing_data = *new std::vector<unsigned long long>; for (size_t i = 0; i < g_profiler_lut.size(); ++i) { std::cout << "函数: " << g_profiler_lut[i] << " 总耗时: " << timing_data[i] << " 微秒\n"; } return 0; }
关键说明
- 编译时索引:
get_profiler_index模板函数为每个调用它的函数生成唯一的静态索引,这个索引在编译阶段就已确定(静态变量的初始化在程序启动前完成)。 - LUT自动注册:每个函数第一次被调用时,静态lambda会将函数名插入全局LUT,保证所有待分析函数的名称都被收集。
- 性能优势:使用
vector的索引访问代替unordered_map的哈希查找,完全消除了字符串哈希和键值对查找的开销,性能提升显著。 - 跨平台兼容:使用C20标准的
std::source_location获取函数名,若编译器不支持C20,可替换为__builtin_FUNCTION()等编译器内置宏。
若你坚持要完全在编译时生成固定大小的LUT(而非运行时动态填充),可利用C++20的constexpr vector或模板元编程实现,但这种方式需要显式声明所有待分析函数,灵活性较差。上述方案兼顾了编译时确定索引和运行时自动收集的便利性,是性能分析器常用的优化方式。
内容的提问来源于stack exchange,提问作者riccardoventrella

