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

本文档介绍了有关构建编译时集合的积极讨论 使用 C++ 编写大量对象。以下使用场景示例展示了 集合很有用:

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

以下部分讨论了每种用例的常见和独特要求, 介绍目前在实现方面面临的挑战,以及建议的解决方案。

StringRef

StringRef 是一种实现字符串引用概念的类型。字符串 reference 是从数字 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 确实能正确处理这些类型的区段属性。