I'm trying to implement an atomic tagged/packed pointer, for the sake of learning.
I want to use the upper 16 bits for a uint16_t
counter, and the lower 3 bits for a 3-bit tag.
So far, I've managed to get everything working except the ability to increment the counter. I'm not very familiar with bitwise operations, so I assume the mistake is probably somewhere in my use of them.
The implementation I have is below:
- (Godbolt link: https://godbolt.org/z/ez7PnWxjG)
The current output is:
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
What I'm trying to achieve is that count=1
when it's printed the second time,
and that we see that stored in the upper 16 bits.
#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();
}