简体   繁体   中英

Overloading operator[] to start at 1 and performance overhead

I am doing some C++ computational mechanics (don't worry, no physics knowledge required here) and there is something that really bothers me.

Suppose I want to represent a 3D math Vector (nothing to do with std::vector):

class Vector {
    public:
        Vector(double x=0., double y=0., double z=0.) { 
            coordinates[0] = x;
            coordinates[1] = y;
            coordinates[2] = z;
        }
    private:
        double coordinates[3];
};

So far so good. Now I can overload operator[] to extract coordinates:

double& Vector::operator[](int i) {
     return coordinates[i] ;
}

So I can type:

Vector V; 

… //complex computation with V

double x1 = V[0];
V[1] = coord2;

The problem is, indexing from 0 is NOT natural here. I mean, when sorting arrays, I don't mind, but the fact is that the conventionnal notation in every paper, book or whatever is always substripting coordinates beginning with 1. It may seem a quibble but the fact is that in formulas, it always takes a double-take to understand what we are taking about. Of course, this is much worst with matrices.

One obvious solution is just a slightly different overloading :

double& Vector::operator[](int i) {
     return coordinates[i-1] ;
}

so I can type

double x1 = V[1];
V[2] = coord2;

It seems perfect except for one thing: this i-1 subtraction which seems a good candidate for a small overhead. Very small you would say, but I am doing computationnal mechanics, so this is typically something we couldn't afford.

So now (finally) my question: do you think a compiler can optimize this, or is there a way to make it optimize ? (templates, macro, pointer or reference kludge...)

Logically, in

double xi = V[i];

the integer between the bracket being a literal most of the time (except in 3-iteration for loops), inlining operator[] should make it possible, right ?

(sorry for this looong question)

EDIT:

Thanks for all your comments and answers

I kind of disagree with people telling me that we are used to 0-indexed vectors. From an object-oriented perspective, I see no reason for a math Vector to be 0-indexed because implemented with a 0-indexed array. We're not suppose to care about the underlying implementation. Now, suppose I don't care about performance and use a map to implement Vector class. Then I would find it natural to map '1' with the '1st' coordinate.

That said I tried out with 1-indexed vectors and matrices, and after some code writing, I find it not interacting nicely every time I use an array around. I thougth Vector and containers (std::array,std::vector...) would not interact often (meaning, transfering data between one another), but it seems I was wrong.

Now I have of a solution that I think is less controversial (please give me your opinion) : Every time I use a Vector in some physical context, I think of using an enum :

enum Coord {
    x = 0,
    y = 1,
    z = 2
};
Vector V;
V[x] = 1;

The only disadvantage I see being that these x,y and z can be redefined without enven a warning...

This one should be measured or verified by looking at the disassembly, but my guess is: The getter function is tiny and its arguments are constant. There is a high chance the compiler will inline the function and constant-fold the subtraction. In that case the runtime cost would be zero.

Why not to try this:

class Vector {
    public:
        Vector(double x=0., double y=0., double z=0.) { 
            coordinates[1] = x;
            coordinates[2] = y;
            coordinates[3] = z;
        }
    private:
        double coordinates[4];
};

If you are not instantiating your object in quantities of millions, then the memory waist might be affordable.

Have you actually profiled it or examined the generated code? That's how this question is answered.

If the operator[] implementation is visible then this is likely to be optimized to have zero overhead.

I recommend you define this in the header (.h) for your class. If you define it in the .cpp then the compiler can't optimize as much. Also, your index should not be an "int" which can have negative values... make it a size_t:

class Vector {

    // ...

public:
    double& operator[](const size_t i) {
        return coordinates[i-1] ;
    }

};

You cannot say anything objective about performance without benchmarking. On x86, this subtraction can be compiled using relative addressing, which is very cheap. If operator[] is inlined, then the overhead is zero—you can encourage this with inline or with compiler-specific instructions such as GCC's __attribute__((always_inline)) .

If you must guarantee it, and the offset is a compile-time constant, then using a template is the way to go:

template<size_t I>
double& Vector::get() {
    return coordinates[i - 1];
}

double x = v.get<1>();

For all practical purposes, this is guaranteed to have zero overhead thanks to constant-folding. You could also use named accessors:

double Vector::x() const { return coordinates[0]; }
double Vector::y() const { return coordinates[1]; }
double Vector::z() const { return coordinates[2]; }

double& Vector::x() { return coordinates[0]; }
double& Vector::y() { return coordinates[1]; }
double& Vector::z() { return coordinates[2]; }

And for loops, iterators:

const double* Vector::begin() const { return coordinates; }
const double* Vector::end() const { return coordinates + 3; }

double* Vector::begin() { return coordinates; }
double* Vector::end() { return coordinates + 3; }

// (x, y, z) -> (x + 1, y + 1, z + 1)
for (auto& i : v) ++i;

Like many of the others here, however, I disagree with the premise of your question. You really should simply use 0-based indexing, as it is more natural in the realm of C++. The language is already very complex, and you need not complicate things further for those who will maintain your code in the future.

Seriously, benchmark this all three ways (ie, compare the subtraction and the double[4] methods to just using zero-based indices in the caller).

It's entirely possible you'll get a huge win from forcing 16-byte alignment on some cache architectures, and equally possible the subtraction is effectively free on some compiler/instruction set/code path combinations.

The only way to tell is to benchmark realistic code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM