Domanda

Ho una libreria C con numerose routine matematiche per trattare vettori, matrici, quaternioni e così via. Deve rimanere in C perché lo uso spesso per il lavoro incorporato e come estensione Lua. Inoltre, ho wrapper di classe C ++ per consentire una più comoda gestione degli oggetti e un sovraccarico dell'operatore per le operazioni matematiche utilizzando l'API C. Il wrapper è costituito solo da un file di intestazione e viene sfruttato il più possibile l'inline.

Esiste una penalità apprezzabile per il wrapping del codice C rispetto al porting e l'integrazione dell'implementazione direttamente nella classe C ++? Questa libreria viene utilizzata in applicazioni time-critical. Quindi, la spinta dall'eliminazione dell'indirizzamento compensa il mal di testa di manutenzione di due porte?

Esempio di interfaccia C:

typedef float VECTOR3[3];

void v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs);

Esempio di wrapper C ++:

class Vector3
{
private:
    VECTOR3 v_;

public:
    // copy constructors, etc...

    Vector3& operator+=(const Vector3& rhs)
    {
        v3_add(&this->v_, this->v_, const_cast<VECTOR3> (rhs.v_));
        return *this;
    }

    Vector3 operator+(const Vector3& rhs) const
    {
        Vector3 tmp(*this);
        tmp += rhs;
        return tmp;
    }

    // more methods...
};
È stato utile?

Soluzione

Il wrapper stesso sarà integrato, tuttavia le chiamate del metodo alla libreria C in genere no. (Ciò richiederebbe ottimizzazioni dei tempi di collegamento che sono tecnicamente possibili, ma al massimo da AFAIK rudimentali negli strumenti di oggi)

Generalmente, una chiamata di funzione in quanto tale non è molto costosa. Il costo del ciclo è notevolmente diminuito negli ultimi anni e può essere facilmente previsto, quindi la penalità di chiamata in quanto tale è trascurabile.

Tuttavia, l'inline apre le porte a più ottimizzazioni: se hai v = a + b + c, la tua classe wrapper forza la generazione di variabili stack, mentre per le chiamate incorporate, la maggior parte dei dati può essere conservata nell'FPU pila. Inoltre, il codice integrato consente di semplificare le istruzioni, considerando valori costanti e altro ancora.

Quindi, sebbene la regola prima di investire sia vera, mi aspetto un certo margine di miglioramento qui.


Una soluzione tipica è quella di portare l'attrezzo C in un formato che può essere usato come funzioni in linea o come "C". corpo:

// V3impl.inl
void V3DECL v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs)
{
    // here you maintain the actual implementations
    // ...
}

// C header
#define V3DECL 
void V3DECL v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs);

// C body
#include "V3impl.inl"


// CPP Header
#define V3DECL inline
namespace v3core {
  #include "V3impl.inl"
} // namespace

class Vector3D { ... }

Questo probabilmente ha senso solo per metodi selezionati con corpi relativamente semplici. Sposterei i metodi in uno spazio dei nomi separato per l'implementazione C ++, poiché di solito non ne avrai bisogno direttamente.

(Nota che l'inline è solo un suggerimento del compilatore, non impone che il metodo sia inline. Ma va bene: se la dimensione del codice di un ciclo interno supera la cache delle istruzioni, l'allineamento danneggia facilmente le prestazioni)

Se il passaggio / ritorno per riferimento può essere risolto dipende dalla forza del tuo compilatore, ho visto molti dove     pippo (X * fuori) forza le variabili dello stack, mentre     X foo () mantiene i valori nei registri.

Altri suggerimenti

Se stai semplicemente racchiudendo le chiamate della libreria C nelle funzioni di classe C ++ (in altre parole, le funzioni C ++ non fanno altro che chiamare le funzioni C), il compilatore ottimizzerà queste chiamate in modo che non si tratti di una penalità prestazionale.

Come per qualsiasi domanda sulle prestazioni, ti verrà chiesto di misurare per ottenere la tua risposta (e questa è la risposta strettamente corretta).

Ma come regola generale, per semplici metodi inline che possono essere effettivamente incorporati, non vedrai alcuna penalità di prestazione. In generale, un metodo inline che non fa altro che passare la chiamata a un'altra funzione è un ottimo candidato per l'inline.

Tuttavia, anche se i metodi del wrapper non fossero in linea, sospetto che non si noterebbe alcuna penalità in termini di prestazioni - nemmeno misurabile - a meno che il metodo wrapper non fosse chiamato in un ciclo critico. Anche allora sarebbe probabilmente misurabile solo se la funzione wrapping stessa non avesse fatto molto lavoro.

Questo tipo di cose riguarda l'ultima cosa di cui preoccuparsi. Innanzitutto preoccupati di rendere il codice corretto, gestibile e di utilizzare algoritmi appropriati.

Come al solito con tutto ciò che riguarda l'ottimizzazione, la risposta è che devi misurare le prestazioni stesse prima di sapere se l'ottimizzazione è utile.

  • Effettua il benchmark di due diverse funzioni, una che chiama direttamente le funzioni in stile C e un'altra che chiama tramite il wrapper. Scopri quale corre più veloce o se la differenza rientra nel margine di errore della tua misurazione (il che significherebbe che non c'è differenza che puoi misurare).
  • Guarda il codice assembly generato dalle due funzioni nel passaggio precedente (su gcc, usa -S o -save-temps ). Vedi se il compilatore ha fatto qualcosa di stupido o se i tuoi wrapper hanno qualche bug prestazionale.

A meno che la differenza di prestazioni non sia troppo grande a favore del non utilizzo del wrapper, la reimplementazione non è una buona idea, dal momento che si rischia di introdurre bug (che potrebbero anche causare risultati che sembrano sani ma sbagliati). Anche se la differenza è grande, sarebbe più semplice e meno rischioso ricordare che C ++ è molto compatibile con C e usa la tua libreria in stile C anche all'interno del codice C ++.

Non credo che noterai molta differenza. Supponendo che la tua piattaforma di destinazione supporti tutti i tuoi tipi di dati,

Sto codificando per il DS e alcuni altri dispositivi ARM e i punti mobili sono malvagi ... Ho dovuto scrivere float su FixedPoint < 16,8 >

Se sei preoccupato che l'overhead delle funzioni di chiamata ti stia rallentando, perché non testare l'integrazione del codice C o trasformarlo in macro?

Inoltre, perché non migliorare la correttezza const del codice C mentre ci sei - const_cast dovrebbe davvero essere usato con parsimonia, specialmente sulle interfacce che controlli.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top