在编译时进行字符串驻留以进行分析

String-interning at compiletime for profiling

上下文

我正在开发一个仪器分析器,它使您能够按字符串命名不同的测量值。例如:

MEASURE_SCOPE(text_rendering_code);
...
MEASURE_SCOPE(password_hashing);
...
MEASURE_START(system_call);
...
MEASURE_STOP(system_call);

宏定义如下:

#define MEASURE_START(name) save_start_event(get_timestamp(), #name);
#define MEASURE_STOP(name) save_stop_event(get_timestamp(), #name);
#define MEASURE_SCOPE(name) Profiling_Class object##name (#name);

class Profiling_Class{
    string name;
    Profiling_Class(string name){
        this->name = name; 
        save_start_event(get_timestamp(), name);
    }
    ~Profiling_Class(){save_end_event(get_timestamp(), this->name);}
}

save_start_eventsave_end_event 只会将时间戳和名称一起放入某个全局缓冲区以供以后使用(导出测量值等)。

问题是:将测量名称与测量本身一起保存效率非常低。还需要做很多工作来配对 MEASURE_STARTMEASURE_STOP,因为检查它们的名称是否相同需要进行字符串比较。一个更好的解决方案是实习字符串,即在某个地方有一些数组来保存所有字符串:

std::vector<string> = {"text_rendering_code", "password_hashing", "system_call"};

并将测量宏中的字符串替换为数组中字符串的索引:

MEASURE_SCOPE(0);
...
MEASURE_SCOPE(1);
...
MEASURE_START(2);
...
MEASURE_STOP(2);

这种方式需要更少的存储空间,并且检查名称是否匹配成为一个简单的整数比较。另一方面,这对用户来说是非常不友好的,因为他必须事先知道他要给自己测量的名字的索引。

问题

有没有办法保留 MEASURE_SCOPE(text_rendering_code) 的良好用法并自动将其替换为更高效的 MEASURE_SCOPE(0)?这将需要在编译时构建名称数组,有效地驻留字符串。这可能吗?

我只能猜测你的意思,因为你没有提供足够的细节,而且它们很重要。

一种可能的方法是使用您自己的生成器生成一些 ad-hoc C 或 C++ 代码。请记住,可以生成项目的一些 C 或 C++ 代码(这是一种粗略的形式或 metaprogramming; Qt moc, RPCGEN, bison, SWIG are typical examples of C++ or C generators, but you can easily make your own one, see here; perhaps with the help of some scripting language like Python, Guile, AWK, ..., or even in C++), and your build automation could handle that (e.g. some as-hoc rule or recipe in your Makefile)。

然后你可以编写一个非常简单的生成程序,收集代码中所有出现的 MEASURE_SCOPEMEASURE_STARTMEASURE_STOP 宏调用(项目的 *.cpp 文件).这很容易编写代码:您可以逐行读取所有 .cpp 文件并查找 MEASURE_SCOPE (等等...)后跟空格然后是 ( 在其中。

那个生成程序 - 处理你的 interned 字符串 - 可能会发出一个大的 header 文件 measure-generated.h,例如像

这样的东西
// in generated header
#define MEASURE_POINT_system_call 1
#define MEASURE_POINT_password_hashing 2

(也许您想生成一些大的 enum

它还会发出一个 measure-generated-array.cpp 文件,例如

// generated code
const char* measure_array[] = {
  NULL,
  "system_call",
  "password_hashing",
  /// etc....
  NULL,
};

然后你可以在你的一些 headers

#define MEASURE_SCOPE(X) measure_array[MEASURE_POINT_##X]

等,使用 stringizing and/or concatenation

等预处理器技巧

另见 this

This would require building the name-array at compile time, effectively interning the strings. Is this possible?

当然可以。在你自己的 C++ 生成器中执行此操作,它知道你所有的项目 *.cpp 文件,就像我建议的那样。您可以在 build 时间生成 C++ 文件。

不保证相同的文字字符串是相同的,但您可以从中构建可以比较相同的类型(不比较字符串),例如:

// Sequence of char
template <char...Cs> struct char_sequence
{
    template <char C> using push_back = char_sequence<Cs..., C>;
};

// Remove all chars from char_sequence from '[=10=]'
template <typename, char...> struct strip_sequence;

template <char...Cs>
struct strip_sequence<char_sequence<>, Cs...>
{
    using type = char_sequence<Cs...>;
};

template <char...Cs, char...Cs2>
struct strip_sequence<char_sequence<'[=10=]', Cs...>, Cs2...>
{
    using type = char_sequence<Cs2...>;
};

template <char...Cs, char C, char...Cs2>
struct strip_sequence<char_sequence<C, Cs...>, Cs2...>
{
    using type = typename strip_sequence<char_sequence<Cs...>, Cs2..., C>::type;
};

// struct to create a aligned char array
template <typename chars> struct static_string;

template <char...Cs>
struct static_string<char_sequence<Cs...>>
{
    static constexpr char str[sizeof...(Cs)] = {Cs...};
};

template <char...Cs>
constexpr 
char static_string<char_sequence<Cs...>>::str[sizeof...(Cs)];

// helper to get the i_th character (`[=10=]` for out of bound)
template <std::size_t I, std::size_t N>
constexpr char at(const char (&a)[N]) { return I < N ? a[I] : '[=10=]'; }

// helper to check if the c-string will not be truncated
template <std::size_t max_size, std::size_t N>
constexpr bool check_size(const char (&)[N])
{
    static_assert(N <= max_size, "string too long");
    return N <= max_size;
}

// Helper macros to build char_sequence from c-string
#define PUSH_BACK_8(S, I) \
    ::push_back<at<(I) + 0>(S)>::push_back<at<(I) + 1>(S)> \
    ::push_back<at<(I) + 2>(S)>::push_back<at<(I) + 3>(S)> \
    ::push_back<at<(I) + 4>(S)>::push_back<at<(I) + 5>(S)> \
    ::push_back<at<(I) + 6>(S)>::push_back<at<(I) + 7>(S)>

#define PUSH_BACK_32(S, I) \
        PUSH_BACK_8(S, (I) + 0) PUSH_BACK_8(S, (I) + 8) \
        PUSH_BACK_8(S, (I) + 16) PUSH_BACK_8(S, (I) + 24)

#define PUSH_BACK_128(S, I) \
    PUSH_BACK_32(S, (I) + 0) PUSH_BACK_32(S, (I) + 32) \
    PUSH_BACK_32(S, (I) + 64) PUSH_BACK_32(S, (I) + 96)

// Macro to create char_sequence from c-string (limited to 128 chars)
#define MAKE_CHAR_SEQUENCE(S) \
    strip_sequence<char_sequence<> \
    PUSH_BACK_128(S, 0) \
    >::type::template push_back<check_size<128>(S) ? '[=10=]' : '[=10=]'>

// Macro to return an static c-string
#define MAKE_STRING(S) \
    aligned_string<MAKE_CHAR_SEQUENCE(S)>::str

所以

MEASURE_SCOPE(MAKE_STRING("text_rendering_code"));

仍然return与您可以直接比较的指针相同。

您可以修改宏 MEASURE_SCOPE 以直接包含 MAKE_STRING

gcc 有一个 扩展 来简化 MAKE_STRING:

template <typename CHAR, CHAR... cs>
const char* operator ""_c() { return static_string<cs...>{}::str; }

然后

MEASURE_SCOPE("text_rendering_code"_c);