Alcuni compilatori fanno questa ottimizzazione per le chiamate virtuali?

1

Questo mi è venuto in mente, e non sono proprio sicuro di come cercare questo.

Diciamo che hai le seguenti classi

class A
{
public:
    virtual Foo() = 0;

    virtual ManyFoo(int N) 
    {
        for (int i = 0; i < N; ++i) Foo();
    } 
};

class B : public A
{
public:
    virtual Foo()
    {
        // Do something
    }
};

Qualche compilatore crea una versione di ManyFoo() per B che incorpora la chiamata a B::Foo() ?

In caso contrario, fare in modo che B una classe finale abiliti questa ottimizzazione?

Modifica: Mi chiedevo specificamente se questo fosse fatto ovunque per le chiamate virtuali (quindi, a parte quando l'intera chiamata a ManyFoo() è in linea).

    
posta Bwmat 06.09.2018 - 10:06
fonte

3 risposte

5

Credo che il termine che stai cercando sia "devirtualizzazione".

Ad ogni modo, ci hai provato? Se metti questo esempio in Compiler Explorer :

extern void extCall ();

class A
{
public:
    virtual void Foo() const = 0;

    virtual void ManyFoo(int N) const
    {
        for (int i = 0; i < N; ++i) Foo();
    } 
};

class B final : public A
{
public:
    virtual void Foo() const
    {
        extCall ();
    }
};

void b_value_foo (B b) {
    b.ManyFoo (6);
}

void b_ref_foo (B const & b) {
    b.ManyFoo (6);
}

void b_indirect_foo (B b) {
    b_ref_foo (b);
}

... GCC è in grado di produrre quanto segue con -Os :

b_value_foo(B):
        push    rax
        call    extCall()
        call    extCall()
        call    extCall()
        call    extCall()
        call    extCall()
        pop     rdx
        jmp     extCall()
b_ref_foo(B const&):
        mov     rax, QWORD PTR [rdi]
        mov     esi, 6
        mov     rax, QWORD PTR [rax+8]
        jmp     rax
b_indirect_foo(B):
        jmp     b_ref_foo(B const&)

Inline attraverso la chiamata virtuale quando è sicuro al 100% del tipo concreto dell'oggetto b (n.b se cambiamo -Os in -O2 sarà anche completamente in linea b_indirect_foo ). Ma non può essere sicuro del tipo concreto di un oggetto che può vedere solo con un riferimento che non può risalire a un'istanza, e non sembra fidarsi delle annotazioni final per sovrascriverlo (probabilmente perché questo sarebbe molto ABI-fragile, personalmente non lo vorrei). Si fiderà delle annotazioni final sulle funzioni dei membri anche se , ma il tuo esempio preclude quello con la sua struttura.

GCC ha avuto questa ottimizzazione per diverse versioni. Clang e MSVC non sembrano farlo in questo caso (ma pubblicizzano la funzione), quindi la potenza varia chiaramente tra esempi e compilatori.

    
risposta data 06.09.2018 - 12:16
fonte
1

È una buona ipotesi, ma non necessariamente vera, che quando questo- > ManyFoo () ha chiamato l'implementazione all'interno di A, l'implementazione di this- > Foo () sarà anche quella all'interno di A. Quindi il compilatore potrebbe genera pseudo-code per ManyFoo come questo:

if (&this->Foo == &A->Foo) {
    for (int i = 0; i < N; ++i)
        inlined A->Foo();
} else {
    for (int i = 0; i < N; ++i)
        virtual this->Foo();
}

Il compilatore potrebbe anche prendere l'indirizzo di this- > Foo () una volta, quindi chiamare quel puntatore di funzione invece di this- > Foo (), se è più veloce. Il compilatore potrebbe anche solo inline la chiamata a Foo () all'interno di ManyFoo (), e ogni volta che Foo () è sovraccarico, creare una nuova versione di ManyFoo ().

Ho visto macchine virtuali Java che hanno deciso in fase di esecuzione cosa inline, tenendo traccia per un po 'di tempo che di solito viene chiamata l'implementazione e quindi inlining (ovviamente in modo sicuro, quindi se è stata chiamata una diversa implementazione di Foo, funzionerebbe, ma più lento). Quindi se si finisce per chiamare C- > Foo () nel 99% dei casi, allora quel caso verrebbe controllato e in linea. Questo non sarebbe abbastanza intelligente da avere una versione inline per la classe C e una per la classe D.

    
risposta data 06.09.2018 - 16:15
fonte
0

Sì, tipo di. Poiché i tuoi metodi sono definiti in linea, a volte possono essere sottolineati. Né Clang né GCC creano un B::ManyFoo(int) specializzato, però.

Ho modificato il codice per evitare ottimizzazioni inadatte e illustrare alcuni comportamenti:

struct A {
    virtual int Foo() = 0;
    virtual int ManyFoo(int N)  {
        int res = 0;
        for (int i = 0; i < N; ++i) res += Foo();
        return res;
    } 
};

struct B : A {
    virtual int Foo() { return 3; }
};

B force_code_generation() { return {}; }

int dynamic_dispatch(A& object) { return object.ManyFoo(2); }
int static_dispatch (B value)   { return value .ManyFoo(2); }

In un sito non virtuale dove il compilatore è in grado di eseguire la spedizione statica, ManyFoo() e Foo() possono essere completamente appiattiti. GCC 8.2 con -O2 è in grado di valutare la funzione in fase di compilazione:

mov eax, 6
ret

Ma Clang non sembra fare questa ottimizzazione. Integra semplicemente la chiamata ManyFoo(2) , che utilizza chiamate virtuali per richiamare Foo() . Pseudo-codice:

static_dispatch(B* rdi):
        push    rbp
        push    rbx
        push    rax

        rbx = rdi

        rax = *rbx  // load vtable
        eax = call rax[0](rdi)  // first Foo() call
        ebp = eax

        rax = *rbx  // load vtable
        rdi = rbx  // move this pointer to rdi
        eax = call rax[0](rdi)
        eax += ebp  // add the Foo() results

        rsp += 8  // discard saved rax
        pop     rbx
        pop     rbp
        return eax

Con la spedizione dinamica queste ottimizzazioni non sono generalmente possibili. Clang non aggiunge ottimizzazioni speciali e utilizza semplicemente le normali chiamate virtuali. Tuttavia, GCC 8.2 aggiunge guardie ai callites virtuali per integrare facoltativamente la funzione virtuale. Ecco l'assembly generato riscritto come pseudo-codice e riordinato per chiarezza:

dynamic_dispatch(A& rdi):
        rax = *rdi  // load vtable from object
        rdx = rax[8]  // load ManyFoo(int) vtable entry
        // check if ManyFoo(int) method is A::ManyFoo(int)
        if (rdx != &A::ManyFoo(int)) {
            // fallback for virtual ManyFoo(2) call, and return
            esi = 2
            goto rdx  // tailcall
        }

        // We are now in the specialized A::ManyFoo(2) version.
        // The loop for N=2 is unrolled.
        push    rbp
        push    rbx
        rsp -= 8

        // first Foo() call:
        // check if Foo() is B::Foo(), else fall back to virtual call
        ebx = 3  // result of the first B::Foo() call if it is inlined
        rax = rax[0]  // load Foo() vtable entry
        if (rax != &B::Foo()) {
            // fallback for first virtual Foo() call
            rbp = rdi
            eax = call rax(rdi)
            ebx = eax  // save result of first call

            // second Foo() call:
            // check again if Foo() is B::Foo()
            // Can "this" even change its type???
            rax = *rbp
            rax = rax[0]
            if (rax != &B::Foo()) {
                // fallback for second virtual Foo() call
                rdi = rbp
                eax = call rax(rdi)
                goto end
            }
        }

        eax = 3  // result of second B::Foo() call

end:
        // add the result of the calls and return
        eax += ebx
        rsp += 8
        pop     rbx
        pop     rbp
        return eax

Né Clang né GCC cambiano il codice generato a seconda che B sia final .

Fonte: visualizza l'assembly su Explorer del compilatore Godbolt .

    
risposta data 06.09.2018 - 12:13
fonte

Leggi altre domande sui tag