8

I recently asked this question:

Using this pointer causes strange deoptimization in hot loop

The problem was that I was writing to an array of type uint8_t and the compiler treated it as if it could alias with the this pointer of the method (of type struct T*), because void* and char* (=uint8_t*) can always alias any other pointer in C++. This behaviour caused a missed optimization opportunity. I want to avoid this, of course. So the question is: Can I declare an uint8_t array that enforces strict aliasing, i.e., that the compiler treats as never aliased with any pointer of another type? I.e., I am looking for something like a strict_uint8_t type that is an uint8_t with special aliasing behaviour. Is there a way to achieve this?

Example code to show what I mean, borrowed from other question and simplified. For more details, read the linked question and its accepted answer:

struct T{
   uint8_t* target;
   void unpack3bit(char* source, int size) {
        while(size > 0){
           uint64_t t = *reinterpret_cast<uint64_t*>(source);
           /** `this->target` cannot be cached in a register here but has
               to be reloaded 16 times because the compiler
               thinks that `this->target` could alias with `this` itself.
               What I want is a special uint8_t type that does not trigger
               this behaviour. */
           this->target[0] = t & 0x7; 
           this->target[1] = (t >> 3) & 0x7;
           this->target[2] = (t >> 6) & 0x7;
           this->target[3] = (t >> 9) & 0x7;
           this->target[4] = (t >> 12) & 0x7;
           this->target[5] = (t >> 15) & 0x7;
           this->target[6] = (t >> 18) & 0x7;
           this->target[7] = (t >> 21) & 0x7;
           this->target[8] = (t >> 24) & 0x7;
           this->target[9] = (t >> 27) & 0x7;
           this->target[10] = (t >> 30) & 0x7;
           this->target[11] = (t >> 33) & 0x7;
           this->target[12] = (t >> 36) & 0x7;
           this->target[13] = (t >> 39) & 0x7;
           this->target[14] = (t >> 42) & 0x7;
           this->target[15] = (t >> 45) & 0x7;
           source+=6;
           size-=6;
           target+=16;
        }
}
};
Community
  • 1
  • 1
gexicide
  • 38,535
  • 21
  • 92
  • 152
  • 1
    Are you sure you want to do `target += 16`? Then you will loose the original T::target` pointer. – Some programmer dude Oct 10 '14 at 10:49
  • 1
    I have to admit that I don’t understand what makes aliasing with `this` (rather than any other variable) special. I’m also not sure whether this is relevant. Definitely an interesting question anyway. – Konrad Rudolph Oct 10 '14 at 10:49
  • @Joachim Pileborg: Yeah, this is fine. Think of `target` as a "current write head" pointer. The beginning of the `target` buffer is stored somewhere else. Apart from that, this code is just for showing the issue :). – gexicide Oct 10 '14 at 10:51
  • 1
    @KonradRudolph: There is nothing special about `this`. You are right, the issue would also be the same with every other pointer. `this` was just the issue in my example in the other question. – gexicide Oct 10 '14 at 10:52
  • Why do you want to use 'this->' as the function is a member and you can use target directly? – Surt Oct 10 '14 at 10:52
  • 1
    @Surt Either way makes no difference in the generated code. – Konrad Rudolph Oct 10 '14 at 10:53
  • maybe you could create something like this: struct myuint8_t { uint8_t value; } – Géza Török Oct 10 '14 at 10:54
  • @Jarod42: You are right. I have now added the necessary cast. – gexicide Oct 10 '14 at 12:01
  • @Niall: No, this is not the source. You can try it yourself with `uint64_t*`. The source was explained well in the answer of the other question. I use `char*` because I only process 6 bytes (since the number of bits (48) must be divisible by 8 and 3), so I need to advance the pointer by 6 bytes after each loop iteration. Advancing a `uint64_t*` by 6 bytes is not possible. – gexicide Oct 10 '14 at 12:08
  • @gexicide: So you may have *aligning* issue ... – Jarod42 Oct 10 '14 at 13:09
  • 1
    Readers note a pitfall: `std::uint8_t` is guaranteed either to have exactly 8 bits and no padding - or to be missing altogether. It is _not_ guaranteed to have the same type as `unsigned char`, which is equal-to- _or-greater-than_ 8 bits. Thus, `uint8_t` may or may not be alias-able - or may not even exist - depending on your implementation. If, conversely to the OP, you need alias-ability, use `unsigned char` and not something that _might_ be equivalent to it on a given machine. – underscore_d Dec 31 '15 at 15:30

3 Answers3

6

You can use a fixed-size enumeration with base type uint8_t:

enum strict_uint8_t : uint8_t {};

If you want to be able to convert to and from uint8_t transparently, you can wrap it in a struct with converting constructor and conversion operator:

struct strict_uint8_t {
    enum : uint8_t {} i;
    strict_uint8_t(uint8_t i) : i{i} {}
    operator uint8_t() const { return i; }
};

This appears to eliminate the aliasing pessimization in gcc and clang: https://godbolt.org/g/9Ta98b

(Note: the previous approach, using a bitfield, worked in gcc but not in clang.)

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • To clarify: the (otherwise utterly redundant) bitfield specification is necessary to remove the potential for aliasing? – Konrad Rudolph Oct 10 '14 at 10:58
  • @KonradRudolph exactly. – ecatmur Oct 10 '14 at 10:58
  • @ecatmur: Is this portable? It seems to work with `gcc`. But try your link using `clang`. It seems as if it would not buy this hack :). It still reloads 16 times. – gexicide Oct 10 '14 at 11:02
  • How portable is this solution? – Niall Oct 10 '14 at 11:03
  • @gexicide huh, yeah. It even does it if I use a bit field of type `unsigned` - its aliasing detector is highly oversensitive. – ecatmur Oct 10 '14 at 11:13
  • 1
    @Niall bit fields are fully portable. It would depend on what you want to do with `target` afterward, but accessing it as `uint8_t` would be fine (because of aliasing, ironically). – ecatmur Oct 10 '14 at 11:14
  • @Stargateur good point, if `uint8_t` exists then `CHAR_BIT` is pretty much guaranteed to be 8. I've revisited this answer though; it turns out that a fixed-size enumeration works even better. – ecatmur Nov 08 '17 at 17:19
  • It is enough to use `uint8_t i;` inside of a struct to disable aliasing. – magras Oct 07 '21 at 13:57
0

In visual studio you can use __declspec(restict) for functions and __restrict for variables to tell the compiler that the pointer is alias free. I believe that in other compilers like GCC there is a __restrict__ attribute (but I'm not sure). For more info see here

rashmatash
  • 1,699
  • 13
  • 23
0

I believe you'll get rid of the aliasing if you pass both pointers through a function where the pointers are declared with restrict. That's non-standard compiler extension though, e.g. in the case of g++:

#include <cstdint>
#include <climits>

struct T{
   uint8_t* target;
  private:
    void unpack3bit(char*__restrict__ source, int size, uint8_t*__restrict__ dst) {
        while(size > 0){
           uint64_t t = *source;
           dst[0] = t & 0x7; 
           dst[1] = (t >> 3) & 0x7;
           dst[2] = (t >> 6) & 0x7;
           dst[3] = (t >> 9) & 0x7;
           dst[4] = (t >> 12) & 0x7;
           dst[5] = (t >> 15) & 0x7;
           dst[6] = (t >> 18) & 0x7;
           dst[7] = (t >> 21) & 0x7;
           dst[8] = (t >> 24) & 0x7;
           dst[9] = (t >> 27) & 0x7;
           dst[10] = (t >> 30) & 0x7;
           dst[11] = (t >> 33) & 0x7;
           dst[12] = (t >> 36) & 0x7;
           dst[13] = (t >> 39) & 0x7;
           dst[14] = (t >> 42) & 0x7;
           dst[15] = (t >> 45) & 0x7;
           source+=6;
           size-=6;
           target+=16;
        }
    }
public:
   void unpack3bit(char* source, int size) {
       unpack3bit(source,size,this->target);
   }

};

void f(int i, T& t, char* source) {
  t.unpack3bit(source, i); 
}

Online: http://goo.gl/SCjpL6

nos
  • 223,662
  • 58
  • 417
  • 506
  • 1
    Note that you don't need an extra method. Simply caching `this->target` in a local variable is enough. However, your suggestion is simply a walkaround for this specific code, not an answer to my question. Of course, I can always work around the issue by caching pointers in local variables. But this is cumbersome and error prone since it is easy to be forgotten. Therefore, I am looking for a type or something comparable which can simply be used for byte arrays without having to worry further. – gexicide Oct 10 '14 at 12:00