3

I am trying to optimize a piece of code by not pointer chasing, as much as I currently am. I want to create a constant offset to add to a stored pointer to get to the next data entry, see below code. However, the data is sitting side a class or struct, containing different data types.

So I get the correct behavior in the below code snippet, ie the output is 1, 2, 3.

#include <iostream>
#include <vector>

// knows nothing about foo
class Readfoo
{ 
    private:
    int offset;
    double* pdouble;

    public:
    void SetPoint(double* apdouble, int aoffset)  
    {
        offset = aoffset;
        pdouble = apdouble;
    };

    const double& printfoo(int aidouble) const
    {
       return *(pdouble + offset*aidouble);
    };
};

// knows nothing about readFoo
struct foo
{ 
    int a[5];
    double b[10];
};

int main() 
{
    // populate some data (choose b [2] or other random entry.).
    std::vector<foo> bar(10);
    bar[0].b[2] = 1;
    bar[1].b[2] = 2;
    bar[2].b[2] = 3;

    // access b[2] for each foo using an offset.
    Readfoo newReadfoo;
    newReadfoo.SetPoint(&(bar[0].b[2]), sizeof(foo)/sizeof(double));
    for(int ii = 0; ii < 3; ii++)
    {        
        std::cout<<"\n"<<newReadfoo.printfoo(ii);
    }
    return 0;
}

My question is two-fold:

  1. Is this code actually legal, and will it ever produce undefined behavior?
  2. If it is not legal is there a way to, without storing the actual pointers, iterate through foo accessing b[2], in the above case, with some form of constant offset (Like adding the number of bits between the starting address of each data entry.)?
Bevan Jones
  • 199
  • 1
  • 2
  • 15
  • 5
    really curious: do you have any evidence that your code performs better than simply accessing `bar[i].b[2];` – 463035818_is_not_an_ai Jan 30 '19 at 13:05
  • btw the title is slightly misleading as you are not iterating over the members of the struct, but you access always the same member while iterating over the elements in the vector – 463035818_is_not_an_ai Jan 30 '19 at 13:07
  • well after reading it again, the title is actually fine (still can be misunderstood) – 463035818_is_not_an_ai Jan 30 '19 at 13:08
  • Why are you doing what you're doing with that pointer arithmetic? What is the *real* problem you try to solve with it? Code for array access will still be generated as code for pointer arithmetic (for any array or pointer `a` and index `i`, the expressions `a[i]` is *exactly* equal to `*(a + i)`). – Some programmer dude Jan 30 '19 at 13:08
  • 2
    what makes me highly doubt that your code is a real optimization is that you dont use any information that the compiler cannot use also. I would actaully be not too surprised if a compiler emits same output for your code and the same loop written by using the vectors `operator[]` – 463035818_is_not_an_ai Jan 30 '19 at 13:10
  • As for any optimizations, have you measured that what you're trying to optimize is a bottleneck in the real code? Have you profiled and benchmarked? It's not a case of premature optimization? Always concentrate on writing good, readable, maintainable and working code first of all. Then if the performance is not up to the requirements (and remember that "good enough" often *is* good enough), then measure to find your bottlenecks and optimize the worst with plenty of documentation (as optimizations tends to create bad code). – Some programmer dude Jan 30 '19 at 13:10
  • I have some module that can't see the struct, but on initialization, pointers are set up to the data. Sorry, the snippet is more asking specifically about the iteration procedure as it would solve my current problem. I can add more detail if required. – Bevan Jones Jan 30 '19 at 13:18
  • @Someprogrammerdude was just talking to someone in the office going to get on that now. – Bevan Jones Jan 30 '19 at 13:19
  • @Someprogrammerdude so I ran tests and it is very definitely pointer chasing and the above code that is the bottleneck. – Bevan Jones Jan 31 '19 at 12:48

1 Answers1

5

No, this is not legal C++. Pointer arithmetic is only defined if you stay inside the same array (or one past its end).

expr.add#4

When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.

  • (4.1) If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.

  • (4.2) Otherwise, if P points to element x[i] of an array object x with n elements, the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i+j] if 0≤i+j≤n and the expression P - J points to the (possibly-hypothetical) element x[i−j] if 0≤i−j≤n.

  • (4.3) Otherwise, the behavior is undefined.

(4.1) does not apply because you're not operating on nullptrs. (4.2) does not apply because you are working with double*, so x in the standard quote must be a double array, i.e. the b member of your struct. Leaving its bounds with pointer arithmetic, according to the remaining (4.3), is undefined behavior.

What you are trying to do here is exactly what a good compiler should (and will) do under the hood anyway:

volatile double output;

void bar(std::vector<foo> bar, int innerOffset)
{
    for (foo& f : bar)
        output = f.b[innerOffset];
}

https://godbolt.org/z/S9qkTf

Notice how the disassembly does the pointer arithmetic you want (because the compiler knows that it works on the target platform). Here is the innermost loop:

.L3:
    movsd   xmm0, QWORD PTR [rax+24+rsi*8]
    add     rax, 104
    movsd   QWORD PTR output[rip], xmm0
    cmp     rdx, rax
    jne     .L3

104 bytes is exactly how big one foo is. The [rax+24+rsi*8] expression is doing all the additional pointer arithmetic for free.

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • OP does stay within the same array, but the pointer is not to an element in the array (but with some offset). I think you are right, I am just not sure about the details – 463035818_is_not_an_ai Jan 30 '19 at 13:13
  • 1
    @user463035818 Added the explanation. – Max Langhof Jan 30 '19 at 13:14
  • OP could use pointers to elements in the array for the arithmetics and only then add the offset to reach the member of the struct, but I guess that just shifts the UB – 463035818_is_not_an_ai Jan 30 '19 at 13:16
  • 1
    @user463035818 Well you would have to apply a member pointer to the `foo` array element you dereferenced and the pointer arithmetic to go to the right inner array index. Which is just a crazy-complicated way of doing `bar[i].b[y]`. – Max Langhof Jan 30 '19 at 13:17
  • @MaxLanghof would there be any change to your answer considering my edited snippet, although I understand it will still not be legal. – Bevan Jones Jan 30 '19 at 13:52
  • @BevanJones Correct, it would still not be legal. And there is no way to make "assume that there is an array of enclosing compound objects around a given `double*` and point into the internals of one of its other elements" legal C++ other than by writing code where the assumption is explicit (i.e. encoded in the types passed). In other words, if you want a function to dig through objects in a `vector`, that function must know about that `vector`. – Max Langhof Jan 30 '19 at 14:06