关于编译时对象集合的讨论

本文档包含有关使用 C++ 构建编译时对象集合的积极讨论。以下用例展示了编译时集合的适用场景:

  • StringRef - 一种类型,支持构建具有关联唯一数字 ID 的字符串标签(用于跟踪)的编译时集合。
  • LockClass - 一种支持构建编译时状态对象集合以用于运行时锁验证的类型。

以下部分讨论了每个用例的通用和独特要求、实现过程中面临的当前挑战,以及建议的解决方案。

字符串引用

StringRef 是一种实现字符串引用概念的类型。字符串引用是从数字 ID 到字符串的映射。使用该映射可以更经济地使用跟踪缓冲区:一个 (id, string) 对在跟踪会话中发出一次,然后后续事件可能会按 ID 引用字符串,而不是以内嵌方式包含完整的字符序列。

以下是 StringRef 的实际应用示例:

#include <lib/ktrace.h>

template <typename Op, typename... Args>
inline DoSomething(Op&& op, Args&&... args) {
    ktrace_probe(TraceAlways, TraceContext::Thread, "DoSomething"_stringref);
    // ...
}

在这里,字符串字面量运算符 _stringref 会返回 StringRef 的实例,该实例提供了将字符串“DoSomething”映射到跟踪函数使用的数字 ID 的工具。

要求

  • 在引用该 ID 的任何跟踪事件之前,每个跟踪会话最多发出一次每个(ID、字符串)映射。理想情况下,在跟踪会话开始时一次性发出整套映射,以避免在时间敏感型代码中间发出映射的开销。不过,这并非硬性要求。
  • 需要密集的 ID 空间,以便下游处理代码可以使用线性预分配的数组或向量来实现 ID 到字符串查找,但这不是硬性要求。
  • 一些唯一标识重复字符串引用的方法,因为模板和内嵌函数及方法必须支持跟踪调用。

当前实现方式

下面简要介绍了当前的 StringRef 实现。

struct StringRef {
    static constexpr int kInvalidId = -1;

    const char* string{nullptr};
    ktl::atomic<int> id{kInvalidId};
    StringRef* next{nullptr};

    int GetId() {
        const int ref_id = id.load(ktl::memory_order_relaxed);
        return ref_id == kInvalidId ? Register(this) : ref_id;
    }

    // Returns the head of the global string ref linked list.
    static StringRef* head() { return head_.load(ktl::memory_order_acquire); }

private:
    // Registers a string ref in the global linked list.
    static int Register(StringRef* string_ref);

    static ktl::atomic<int> id_counter_;
    static ktl::atomic<StringRef*> head_;
};

// Returns an instance of StringRef that corresponds to the given string
// literal.
template <typename T, T... chars>
inline StringRef* operator""_stringref() {
    static const char storage[] = {chars..., '\0'};
    static StringRef string_ref{storage};
    return &string_ref;
}

LockClass

LockClass 是一种类型,可捕获所有锁实例通用的锁相关信息(例如,如果是结构体/类成员,则为其包含类型、底层锁基元的类型、描述其行为的标志)。运行时锁验证器使用 LockClass 类型确定每个锁适用的排序规则,并找到用于记录排序观察结果的按锁类跟踪结构。

以下是 LockClass 实际应用的简化示例:

struct Foo {
    LockClass<Foo, fbl::Mutex> lock;
    // ...
};

struct Bar {
    LockClass<Bar, fbl::Mutex> lock;
};

要求

  • 能够为 LockClass 的所有实例化迭代跟踪状态,以进行周期检测和错误报告。
  • 一些唯一重复跟踪状态的方法,因为 LockClass 的实例化可能在多个编译单元中可见,具体取决于其所属类型(例如 Foo 和 Bar)的使用方式。
  • 需要密集的 ID 空间,以便下游处理代码可以简化 ID 存储,但这不是硬性要求。

当前实现方式

以下是 LockClass 的简化实现:

template <typename ContainingType, typename LockType>
class LockClass {
    // ...
private:
    static LockClassState lock_class_state_;
};

LockClass 的每次实例化都会创建一个唯一的 LockClassState 实例,以跟踪与类(ContainingTypeLockType)锁定相关的在线锁顺序观察。LockClassState 的当前实现会构造一个全局 ctor 中所有实例的链接列表,以支持迭代要求。

编译时数组解决方案

若要满足这两种类型的要求,一种方法是使用 COMDAT 部分和组来构建去重静态实例的编译时数组。这完全不需要在初始化时或运行时构建链接的对象列表,并且支持每种类型的所有要求。

例如:

// Defined in the linker script mark the beginning and end of the section:
// .data.lock_class_state_table.
extern "C" LockClassState __start_lock_class_state_table[];
extern "C" LockClassState __end_lock_class_state_table[];

template <typename ContainingType, typename LockClass>
class LockClass {
    // ...
private:
    static LockClassState lock_class_state_ __SECTION(".data.lock_class_state_table");
};

// Defined in the linker script to make the beginning and end of the section:
// .rodata.string_ref_table.
extern "C" StringRef __start_string_ref_table[];
extern "C" StringRef __end_string_ref_table[];

struct StringRef {
    const char* const string;
    size_t GetId() const {
        return static_cast<size_t>(this - __start_string_ref_table);
    }
};

template <typename T, T... chars>
inline StringRef* operator""_stringref() {
    static const char storage[] = {chars..., '\0'};
    static StringRef string_ref __SECTION(".rodata.string_ref_table") {storage};
    return &string_ref;
}

这种方法面临的挑战是 GCC 无法正确处理模板类型或函数的静态成员的区段属性。不过,Clang 确实会正确处理这些类型的部分属性。