1

Take this toy code:

void f(const int& a, int* dat) {
    
    for (int i=0; i<a; ++i )
      dat[i] += a;
}

Observe that the compiler is afraid that dat[i] might alias with a, i.e. writing to dat[i] might change the value of a - and so it feels obliged to re-load a on every loop iteration (that's the 'movslq (%rdi), %rax' line at the link).

This could be solved with strict-aliasing, by changing the type of dat:

void f(const int& a, long* dat) {
...    

The generated code seems Indeed longer, but that's due to vectorization. It doesn't re-load a on every iteration.

I'm surprised this doesn't work for my own custom type!

struct myInt {
    int i;
    myInt& operator+=(int rhs) { i += rhs; return *this;}
};

void f(const int& a, myInt* dat) {
    
    for (int i=0; i<a; ++i )
      dat[i] += a;
}

Here the compiler returns to re-loading the loop boundary on every iteration. Both clang and gcc do.

Looks like the compiler's alias-analysis treats myInt as a regular int - which is reasonable in many aspects, but causes this optimization to be missed. Could this be a compiler bug? Or is there something I'm missing about strict-aliasing?

Ofek Shilon
  • 14,734
  • 5
  • 67
  • 101

2 Answers2

4

Imagine the following:

struct myInt {
    int i;
    myInt& operator+=(int rhs) { i += rhs; return *this;}
};

void f(const int& a, myInt* dat) 
{
    for (int i=0; i<a; ++i)
      dat[i] += a;
}

int main()
{
   myInt foo{ 1 };
   f(foo.i, &foo);
}

In this program, a and dat.i actually alias. They are the same variable. So the compiler actually needs to reload one after a write to the other.

Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
Jeffrey
  • 11,063
  • 1
  • 21
  • 42
  • That could have been said for the second (optimized) code as well: `int a = 1; f(a, (long*)&a)`. And yet the compiler optimizes it. Strict-aliasing is exactly the permission we give the compiler to assume that if two objects have different types they do not alias. – Ofek Shilon Jan 16 '22 at 04:05
  • In the code you present, your typecast is a reinterpret cast. If you reinterpret cast values to types they are not, you are _asking_ for aliasing issues (among others) – Jeffrey Jan 16 '22 at 04:06
  • Indeed - and the compiler is not required to protect me from myself in such cases. If the static types of `a` and `dat` differ - the compiler is allowed to deduce they don't alias.. – Ofek Shilon Jan 16 '22 at 04:10
  • 2
    @OfekShilon The snippet here is legal, while `f(a, (long*)&a)` is not. – Passer By Jan 16 '22 at 04:14
  • I think I see your point, thanks. – Ofek Shilon Jan 16 '22 at 05:16
1

If you are sure no aliasing happens, most compilers support restrict from C99:

void f(const int& __restrict a, int* dat) {
    for (int i = 0; i < a; ++i) dat[i] += a;
}

Godbolt link

Note that this is dangerous code. A better option would be to take a by value:

void f(const int a, int* dat) {
    for (int i = 0; i < a; ++i) dat[i] += a;
}

Godbolt link

Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • 1
    Note that both clang and gcc are prone to behave weirdly if pointer-equality operators happen to observe that the address which is based upon a restrict-qualified pointer equals an address which is not based upon that pointer. The way the C Standard is written makes it possible to argue that, given `if (restrictedPointer == otherPointer) *restrictedPointer = 3;`, the pointer which is used conditionally to store the number 3 isn't based upon the value of `restrictedPointer` outside the `if` statement, and both gcc and clang will sometimes treat it as not based upon that value. – supercat Mar 11 '22 at 22:12