C++:原子标记指针,上位有 16 位计数器,似乎无法增加计数器



我正在尝试实现一个原子标记/打包指针,为了学习。

我想用上面的16位作为uint16_t计数器,下面的3位作为3位标签。

到目前为止,我已经设法使一切工作,除了增加计数器的能力。我不是很熟悉按位操作,所以我认为错误可能是在我使用它们的某个地方。

实现如下:

  • (Godbolt链接:https://godbolt.org/z/ez7PnWxjG)

当前输出为:

AtomicTaggedPointer(ptr=0x5589e2dc92b0, val=42, tag=4, count=0) 
0000000000000000 0101010110001001 1110001011011100 1001001010110 100
AtomicTaggedPointer(ptr=0x5589e2dca2e0, val=43, tag=5, count=0) 
0000000000000000 0101010110001001 1110001011011100 1010001011100 101

我想要实现的是,当count=1第二次打印时,我们看到它存储在上面的16位。

#include <atomic>
#include <cassert>
#include <cstdint>
#include <cstdio>
// A word-aligned, atomic tagged pointer.
// Uses both the upper 16 bits for storage, and the lower 3 bits for tagging.
//
//   64                48                32                16
// 0xXXXXXXXXXXXXXXXX  0000000000000000  0000000000000000  0000000000000XXX
//   ^                 ^                                                ^
//   |                 |                                                +-- Tag (3 bits)
//   |                 +-- Pointer (48 bits)
//   +-- Counter (16 bits)
//
//
// The tag is 3 bits, allowing for up to 8 different tags.
//
// The version is incremented every time the pointer is CAS'd. This is useful
// for detecting concurrent modifications to a pointer.
template <typename T>
struct AtomicTaggedPointer
{
static_assert(sizeof(T*) == 8, "T* must be 8 bytes");
static_assert(sizeof(std::atomic<uintptr_t>) == 8, "uintptr_t must be 8 bytes");
private:
static constexpr uintptr_t kTagMask      = 0x7;                // 3 bits
static constexpr uintptr_t kCounterMask  = 0xFFFF000000000000; // 16 bits
static constexpr uintptr_t kPointerMask  = ~kTagMask;          // All bits except tag bits
static constexpr uintptr_t kCounterShift = 48;                 // Shift counter bits to the left
std::atomic<uintptr_t> value;
public:
AtomicTaggedPointer(T* ptr, uint8_t tag = 0)
{
value.store(reinterpret_cast<uintptr_t>(ptr) | tag, std::memory_order_relaxed);
}
T* get() const
{
return reinterpret_cast<T*>(value.load(std::memory_order_relaxed) & kPointerMask);
}
uint8_t tag() const
{
return value.load(std::memory_order_relaxed) & kTagMask;
}
uint16_t counter() const
{
return value.load(std::memory_order_relaxed) >> kCounterShift;
}
// Compare and swap the pointer with the desired value, and optionally set the tag.
// Returns true if the swap was successful.
// Also increments the version counter by 1.
bool cas(T* desired, uint8_t tag = 0)
{
uintptr_t expected = value.load(std::memory_order_relaxed);
uintptr_t desired_value =
reinterpret_cast<uintptr_t>(desired) | (tag & kTagMask) | ((expected + 1) & kCounterMask) << 48;
return value.compare_exchange_strong(expected, desired_value, std::memory_order_relaxed);
}
void print() const
{
printf("AtomicTaggedPointer(ptr=%p, val=%d, tag=%hhu, count=%hu) n", get(), *get(), tag(), counter());
// Print each bit of the pointer's 64-bit value
// In the format:
// 0xXXXXXXXXXXXXXXXX  0000000000000000  0000000000000000  0000000000000XXX
uintptr_t v = value.load(std::memory_order_relaxed);
for (int i = 63; i >= 0; i--)
{
if (i == 2 || i == 15 || i == 31 || i == 47)
{
printf(" ");
}
printf("%lu", (v >> i) & 1);
}
printf("n");
}
};
int
main()
{
AtomicTaggedPointer<int> p = AtomicTaggedPointer<int>(new int(42), 4);
p.print();
assert(p.get() != nullptr);
assert(*p.get() == 42);
assert(p.tag() == 4);
assert(p.counter() == 0);
int* expected = p.get();
p.cas(new int(43), 5);
p.print();
}

通过增加1来增加expected。这是最低位的增量。但那是你放标签的地方。计数器位于最高位。因此,您需要首先将1移位到最低的计数器位,即kCounterShift(并且要这样做,您应该首先将1转换为适当的类型uintptr_t,以便移位确保在范围内)。

另外,你的kPointerMask是错误的,因为它没有掩盖计数器位。

另外,为了确保指针正确对齐,您应该在alignof(T)足够大时添加静态断言。那么你应该没问题,因为在标准c++中不可能为一个类型传递有效的未对齐指针值,甚至在允许未对齐访问的平台上,也不允许像这样传递和解引用不正确对齐的指针。(见答案下面的评论)

在您的特定示例中,int将不满足该要求。它将只有4字节的对齐,而不是您需要的8字节。使用new来创建对象可能会使您免于获得未对齐8字节的指针,但即使对于new int,这通常也不能保证。

当然,这里有很多是实现定义的行为。例如,所使用的指针中的位的布局以及它们如何映射到uintptr_t是特定于x86-64和典型ABI的。我可以想象编译器出于某种目的或类似的目的使用指针中未使用的位。

可能还有一个更广泛的问题,即整个方法是否与指针来源兼容,对此我不确定。

最新更新