Do GCC's function __attribute__s work with virtual functions?
Asked Answered
J

4

13

The GCC C++ compiler offers a family of extensions via function attributes, such as:

int square(int) __attribute__((const));

Two attributes in particular, const and pure, allow you to declare that a function's evaluation has no side effects and depends only on its arguments (const), or only on its arguments and global variables (pure). This allows for common subexpression elimination, which may have the effect that such a function gets called fewer times than it is written in the code.

My question is whether this can be used safely, correctly and sensibly for virtual member functions:

struct Foo
{
    virtual int square(int) __attribute__((pure));   // does that make sense?
};

Does this have any sensible semantics? Is it allowed at all? Or is it just ignored? I'm afraid I can't find the answer to this in the GCC documentation.

The reason for this question is that there is a family of compiler options -Wsuggest-attribute which make GCC produce suggestions of where those attributes might be placed to improve the code. However, it seems to end up making these suggestions even for virtual functions, and I wonder whether those suggestions should be taken seriously.

Jackinthepulpit answered 12/6, 2012 at 9:41 Comment(5)
Can you assign these attributes a pointer to a function, and have it make sense? If the answer to that is yes, then it should work for virtual functions too.Subhuman
Damned, why did they had to twist the exchange the (generally understood) meaning of const and pure :(Yasukoyataghan
@MatthieuM. It's historical: __attribute__((const)) was added a very long time ago, a time when "const" had just been invented, so it didn't have a generally understood meaning, and I guess someone thought equating it with the programming-language-theoretic "pure" made sense. The weaker __attribute__((pure)) was added much later, and at that point there was too much installed base of __attribute__((const)) to swap them.Deportee
@Zack: I understand, and if it was only const, why not. My main issue is with pure. A different name would have been great, since the generally understood concept of purity is that repeatedly invoking the same function with the same parameters should produce the same results, which is broken here by the dependency on global variables.Yasukoyataghan
@MatthieuM. I don't know why they picked that name, only why they didn't give it the name const.Deportee
U
5

The first question is whether these attributes even have valid semantics for virtual methods. In my opinion they do. I would expect that if a virtual function was labelled pure you would be promising the compiler that all implementations only rely on their arguments and the data in global memory (and do not change that), where data in global memory would also include the contents of the object. If a virtual function were labelled const this would mean it could depend on only on its arguments, it would not even be allowed to inspect the contents of the object. The compiler would have to enforce that all overriding virtual methods declare attributes at least as strong as their parents.

The next question is whether GCC uses these attributes to make optimisations. In the following test program you can see that version 4.6.3 does not (try compiling to assembler with -O3 and you will see the loop is unrolled).

struct A {
    virtual int const_f(int x) __attribute__((const)) = 0;
};

int do_stuff(A *a) {
    int b = 0;
    for (int i=0; i<10; i++) {
        b += a->const_f(0);
    }
    return b;
}

Even in the following program where the type is known at compile time the compiler does not optimize the loop.

struct A {
    virtual int const_f(int x) __attribute__((const)) = 0;
};

struct B : public A {
    int const_f(int x) __attribute__((const));
};

int do_stuff(B *b) {
    int c = 0;
    for (int i=0; i<10; i++) {
        c += b->const_f(0);
    }
    return c;
}

Removing the inheritance from A (and thus making the method non-virtual) allows the compiler to make the expected optimisation.

There is no standard or documentation regarding these attributes, so the best reference we can have is the implementation. As they currently have no effect I would suggest avoiding their use on virtual methods in case the behaviour changes unexpectedly in the future.

Uxorious answered 16/8, 2012 at 12:46 Comment(1)
Clang 10 appears to understand __attribute__((const)) in both snippets, and only calls const_f once. GCC 10 is still not smart enough for that.Screech
P
5

It is allowed and accepted by GCC. It is not generally ignored (you know this because GCC always outputs warning: attribute ignored, when it completely ignores an attribute, it doesn't here). But also read the last paragraph.

Whether it makes sense is another question. A virtual function can be overloaded, and you can overload it without the attribute. This opens the next question: Is this legal?

One would expect a function with different attributes to have a different signature (such as with the const qualifier or a different exception specification), but this is not the case. GCC treats them as exactly identical in this respect. You can verify this by deriving Bar from Foo and implementing the member function non-const. Then

decltype(&Bar::square) f1 = &Foo::square;
decltype(&Foo::square) f2 = &Bar::square;

Will give a compile-time error in the second line, but not in the first, just as you would expect. Were the signatures different (try making the function const-qualified, instead of using the attribute!), the first line would already give an error.

Finally, is it safe, and does it make sense? It's always safe, the compiler has to make sure it is. It does make sense semantically, within limits.

From a semantic point of view, it is "correct" to declare a function const or pure if that is what it really is. However, it is kind of awkward insofar as you make a "promise" to the user of the interface which may not be true. Someone might call this function which to all appearances is const on a derived class where this isn't true. The compiler will have to make sure it still works, but user expectations on performance may differ from reality.

Marking functions as const or pure possibly allows the compiler to optimize better. Now, with a virtual function, this is somewhat hard, since an object might be of a derived type where that is not true!
This necessarily means that the compiler must ignore the attribute for optimization unless the virtual call can be resolved statically. This may still often be the case, but not in general.

Photocompose answered 16/8, 2012 at 12:23 Comment(2)
Putting these promises on virtual functions will confuse the users not only with respect to their performance expectations. One might think: "so, this function is marked const, I might as well cache the result instead of calling it multiple times", and end up with invalid results.Qua
@GrzegorzHerman: That's right, this in addition to performance expectations. In this case, the compiler will not even be able to make sure it works.Photocompose
U
5

The first question is whether these attributes even have valid semantics for virtual methods. In my opinion they do. I would expect that if a virtual function was labelled pure you would be promising the compiler that all implementations only rely on their arguments and the data in global memory (and do not change that), where data in global memory would also include the contents of the object. If a virtual function were labelled const this would mean it could depend on only on its arguments, it would not even be allowed to inspect the contents of the object. The compiler would have to enforce that all overriding virtual methods declare attributes at least as strong as their parents.

The next question is whether GCC uses these attributes to make optimisations. In the following test program you can see that version 4.6.3 does not (try compiling to assembler with -O3 and you will see the loop is unrolled).

struct A {
    virtual int const_f(int x) __attribute__((const)) = 0;
};

int do_stuff(A *a) {
    int b = 0;
    for (int i=0; i<10; i++) {
        b += a->const_f(0);
    }
    return b;
}

Even in the following program where the type is known at compile time the compiler does not optimize the loop.

struct A {
    virtual int const_f(int x) __attribute__((const)) = 0;
};

struct B : public A {
    int const_f(int x) __attribute__((const));
};

int do_stuff(B *b) {
    int c = 0;
    for (int i=0; i<10; i++) {
        c += b->const_f(0);
    }
    return c;
}

Removing the inheritance from A (and thus making the method non-virtual) allows the compiler to make the expected optimisation.

There is no standard or documentation regarding these attributes, so the best reference we can have is the implementation. As they currently have no effect I would suggest avoiding their use on virtual methods in case the behaviour changes unexpectedly in the future.

Uxorious answered 16/8, 2012 at 12:46 Comment(1)
Clang 10 appears to understand __attribute__((const)) in both snippets, and only calls const_f once. GCC 10 is still not smart enough for that.Screech
A
3

In the document that you linked to, there's this note under the description of const attribute:

Note that a function that has pointer arguments and examines the data pointed to must not be declared const.

I'd say this includes member functions, since they have an implicit pointer parameter (and at least virtual functions need to examine it to get to the vtable, no?).

They seem to get to a similar conclusion in this thread: http://gcc.gnu.org/ml/gcc/2011-02/msg00460.html

Athenaathenaeum answered 12/6, 2012 at 12:31 Comment(1)
There is no vtable and no "implicit pointer" in C++. Member functions have an opaque, implicit instance reference, though. I suppose the case could be made, but the problem is specifically where a base function is actually pure, but a potential derived override need not be. Does the attribute only affect the function if it can be dispatched statically?Jackinthepulpit
R
2

G++ 4.8.1 appears to respect the pure and const function attributes on virtual member functions if and only if the function is called via a static binding.

Given the following source code:

struct Base {
    void w();
    void x() __attribute__ ((const));
    virtual void y();
    virtual void z() __attribute__ ((const));
};

struct Derived : public Base {
    void w() __attribute__ ((const));
    void x();
    virtual void y() __attribute__ ((const));
    virtual void z();
};

void example() {
    Base b, *pb;
    Derived d, *pd;
    b.w(); // called
    b.x(); // not called
    b.y(); // called
    b.z(); // not called
    pb->w(); // called
    pb->x(); // not called
    pb->y(); // called
    pb->z(); // called
    d.w(); // not called
    d.x(); // called
    d.y(); // not called
    d.z(); // called
    pd->w(); // not called
    pd->x(); // called
    pd->y(); // called
    pd->z(); // called
}

…the compiler produces the following (excerpted) assembly code:

void example() {
    Base b, *pb;
    Derived d, *pd;
    b.w(); // called
  1c:   e8 00 00 00 00          callq  21 <_Z7examplev+0x21>
    b.x(); // not called
    b.y(); // called
  21:   48 89 e7                mov    %rsp,%rdi
  24:   e8 00 00 00 00          callq  29 <_Z7examplev+0x29>
    b.z(); // not called
    pb->w(); // called
  29:   48 89 df                mov    %rbx,%rdi
  2c:   e8 00 00 00 00          callq  31 <_Z7examplev+0x31>
    pb->x(); // not called
    pb->y(); // called
  31:   48 8b 2b                mov    (%rbx),%rbp
  34:   48 89 df                mov    %rbx,%rdi
  37:   ff 55 00                callq  *0x0(%rbp)
    pb->z(); // called
  3a:   48 89 df                mov    %rbx,%rdi
  3d:   ff 55 08                callq  *0x8(%rbp)
    d.w(); // not called
    d.x(); // called
  40:   48 8d 7c 24 10          lea    0x10(%rsp),%rdi
  45:   e8 00 00 00 00          callq  4a <_Z7examplev+0x4a>
    d.y(); // not called
    d.z(); // called
  4a:   48 8d 7c 24 10          lea    0x10(%rsp),%rdi
  4f:   e8 00 00 00 00          callq  54 <_Z7examplev+0x54>
    pd->w(); // not called
    pd->x(); // called
  54:   48 89 df                mov    %rbx,%rdi
  57:   e8 00 00 00 00          callq  5c <_Z7examplev+0x5c>
    pd->y(); // called
  5c:   48 8b 2b                mov    (%rbx),%rbp
  5f:   48 89 df                mov    %rbx,%rdi
  62:   ff 55 00                callq  *0x0(%rbp)
    pd->z(); // called
  65:   48 89 df                mov    %rbx,%rdi
  68:   ff 55 08                callq  *0x8(%rbp)
}
Ria answered 23/8, 2013 at 4:28 Comment(1)
Hm, dereferencing uninitialized pointers is UB, and the compiler may very well take advantage of that, so I'm not sure I trust that output...Jackinthepulpit

© 2022 - 2024 — McMap. All rights reserved.