8

Is there a portable way to implement a tagged pointer in C/C++, like some documented macros that work across platforms and compilers? Or when you tag your pointers you are at your own peril? If such helper functions/macros exist, are they part of any standard or just are available as open source libraries?

Just for those who do not know what tagged pointer is but are interested, it is a way to store some extra data inside a normal pointer, because on most architectures some bits in pointers are always 0 or 1, so you keep your flags/types/hints in those extra bits, and just erase them right before you want to use pointer to dereference some actual value.

const int gc_flag = 1;
const int flag_mask = 7; // aka 0b00000000000111, because on some theoretical CPU under some arbitrary OS compiled with some random compiler and using some particular malloc last three bits are always zero in pointers.

struct value {
   void *data;
};

struct value val;
val.data = &data | gc_flag;
int data = *(int*)(val.data & flag_mask);

https://en.wikipedia.org/wiki/Pointer_tagging

melpomene
  • 84,125
  • 8
  • 85
  • 148
exebook
  • 32,014
  • 33
  • 141
  • 226
  • "because on x86_64..." that's not portable. – kmdreko Nov 26 '17 at 21:10
  • 3
    @vu1p3n0x, you are correct, thus my question is, is it possible to make it portable. – exebook Nov 26 '17 at 21:11
  • 1
    3 does not equal 0b111 – Eljay Nov 26 '17 at 21:12
  • @Eljay, corrected, thanks – exebook Nov 26 '17 at 21:13
  • " because on x86_64 last three bits are always zero in pointers" - how so? are 64-bit processors not byte addressable? –  Nov 26 '17 at 21:14
  • 5
    No, it's not possible to make this portable. Back in the day, 68000 based machines had 32-bit pointers, and the top 8 bits were fallow. So developers would use those for storing meta-data. It was okay... until 68020 and 68030 came out, which used all the bits. Made for a lot of hassle (or job security, depending on your point of view). – Eljay Nov 26 '17 at 21:15
  • @1201ProgramAlarm Not necessarily, the original `p` could have had a nonzero bit 0. Both are possible because `alignof(char) == 1`. If you used `char16_t *p = u"xyz"; ++p;`, both the old an new `p` would have zero for bit 0, because `alignof(char16_t) == 2`. If your pointer is to a `uint64_t`, then the alignment requirement is 8, leaving three bits. There isn't a guaranteed-by-the-standard way of accessing those bits, though. – Daniel H Nov 26 '17 at 21:18
  • @1201ProgramAlarm Yes, you can. And then you're no longer working in standards-compliant C++, because the standard does have alignment requirements. – Daniel H Nov 26 '17 at 21:21
  • 1
    It depends what you mean by "portable". Is there a way to do it which works on Windows, Mac, and Linux on all common processors, at least for types with large enough alignment requirements? Yes, and it doesn't even invoke undefined behavior for those systems because the implementations have design documents for their ABIs that you can reference. Is there a way to do it which is fully compliant with the C++ standard and will work anywhere? No. – Daniel H Nov 26 '17 at 21:22
  • I don't see how this can be portable. The next release of Intel's CPU may decide to raise a hardware exception if the last three bits are anything but zero. Or they may give their own meaning to them. Even the compiler may decide to use them and its usage may change from one compiler release to another. – Galik Nov 26 '17 at 21:24

3 Answers3

5

You can get the lowest N bits of an address for your personal use by guaranteeing that the objects are aligned to multiples of 1 << N. This can be achieved platform-independently by different ways (alignas and aligned_storage for stack-based objects or std::aligned_alloc for dynamic objects), depending on what you want to achieve:

struct Data { ... };

alignas(1 << 4) Data d; // 4-bits, 16-byte alignment
assert(reinterpret_cast<std::uintptr_t>(&d) % 16 == 0);

// dynamic (preferably with a unique_ptr or alike)
void* ptr = std::aligned_alloc(1 << 4, sizeof(Data));
auto obj = new (ptr) Data;
...
obj->~Data();
std::free(ptr);

You pay by throwing away a lot of memory, exponentionally growing with the number of bits required. Also, if you plan to allocate many of such objects contiguously, such an array won't fit in the processor's cacheline for comparatively small arrays, possibly slowing down the program considerably. This solution therefore is not to scale.

Jodocus
  • 7,493
  • 1
  • 29
  • 45
  • 1
    Just don't try doing any pointer arithmetic or using arrays of such objects. And given the overhead resulting from the alignment restrictions, using normal pointer and a flags field in the structure is probably more memory-efficient anyway. `struct value { void *data, uint16_t flags };` perhaps. – Andrew Henle Nov 26 '17 at 22:23
  • 2
    @AndrewHenle "using normal pointer and a flags field in the structure is probably more memory-efficient anyway." Yes, but this may not be the intent if you are e.g. dealing with ABA or memory-reclamation problems in lock-free algorithms. – Jodocus Nov 26 '17 at 22:32
3

If you're sure that the addresses you are passing around always have certain bits unused, then you could use uintptr_t as a transport type. This is an integer type that maps to pointers in the expected way (and will fail to exist on an obscure platform that offers no such possible map).

There aren't any standard macros but you can roll your own easily enough. The code (sans macros) might look like:

void T_func(uintptr_t t)
{
    uint8_t tag = (t & 7);
    T *ptr = (T *)(t & ~(uintptr_t)7);

    // ...
}

int main()
{
    T *ptr = new T;
    assert( ((uintptr_t)ptr % 8) == 0 );
    T_func( (uintptr_t)ptr + 3 );
}

This may defeat compiler optimizations that involve tracking pointer usage.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • 1
    It would be more useful with prototypes of `void *add_tag(void *p, uint8_t tag);` and `void* split_components(void *tagged_ptr, uint8_t *tag_out);` but the approach is solid. – lockcmpxchg8b Nov 26 '17 at 22:26
  • 1
    @lockcmpxchg8b I am suggesting using `uintptr_t` rather than `void *`. The latter would lead to undefined behaviour in the case of allocations smaller than 8 bytes etc. – M.M Nov 26 '17 at 22:27
  • I was proposing to hide the cast to `uintptr_t` within the function, so that a user doesn't have to litter their code with casts. – lockcmpxchg8b Nov 26 '17 at 22:28
  • @lockcmpxchg8b using `uintptr_t add_tag(void *p, uint8_t tag)` and `void *split(uintptr_t tagged, uint8_t *tag_out)` the user does not litter their code with casts either – M.M Nov 26 '17 at 22:33
  • Agree. it wasn't obvious that is your intent. – lockcmpxchg8b Nov 26 '17 at 22:34
1

Well, GCC at least can compute the size of bit-fields, so you can get portability across platforms (I don't have an MSVC available to test with). You can use this to pack the pointer and tag into an intptr_t, and intptr_t is guaranteed to be able to hold a pointer.

#include <limits.h>
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <inttypes.h>

struct tagged_ptr
{
  intptr_t ptr : (sizeof(intptr_t)*CHAR_BIT-3);
  intptr_t tag : 3;
};

int main(int argc, char *argv[])
{
  struct tagged_ptr p;

  p.tag = 3;
  p.ptr = (intptr_t)argv[0];

  printf("sizeof(p):              %zu <---WTF MinGW!\n", sizeof p);
  printf("sizeof(p):              %lu\n", (unsigned long int)sizeof p);
  printf("sizeof(void *):         %u\n", (unsigned int)sizeof (void *));
  printf("argv[0]:                %p\n", argv[0]);
  printf("p.tag:                  %" PRIxPTR "\n", p.tag);
  printf("p.ptr:                  %" PRIxPTR "\n", p.ptr);
  printf("(void *)*(intptr_t*)&p: %p\n", (void *)*(intptr_t *)&p);
}

Gives:

$ ./tag.exe
sizeof(p):              zu <---WTF MinGW!
sizeof(p):              8
sizeof(void *):         8
argv[0]:                00000000007613B0
p.tag:                  3
p.ptr:                  7613b0
(void *)*(intptr_t*)&p: 60000000007613B0

I've put the tag at the top, but changing the order of the struct would put it at the bottom. Then shifting the pointer-to-be-stored right by 3 would implement the OP's use case. Probably make macros for access to make it easier.

I also kinda like the struct because you can't accidentally dereference it as if it were a plain pointer.

lockcmpxchg8b
  • 2,205
  • 10
  • 16
  • Interesting, but the stated goal is portability. I think there's nothing in C *less* portable than bit fields. – Andrew Henle Nov 26 '17 at 22:20
  • Yeah...that might make a function-based approach like M.M's the right choice. – lockcmpxchg8b Nov 26 '17 at 22:23
  • 1
    mingw uses MS' old CRT lib which only supports C89 along with MS extensions. Change to mingw64 instead and `#define __USE_MINGW_ANSI_STDIO 1` to make printf work with C99 format specifiers. [How to printf a size_t without warning in mingw-w64 gcc 7.1?](https://stackoverflow.com/q/44382862/995714), [msvcrt alternative for MinGW? (e.g. to get conforming snprintf)](https://stackoverflow.com/q/57604482/995714) – phuclv Oct 02 '20 at 03:30