Newsgroup: it.comp.lang.c++
Titolo: const ed ottimizzazione

Come accennai anche in un vecchio post, sono interessato a conoscere
le ottimizzazione che può consentire il dichiarare un puntatore const.
Ecco un semplice esempio:
struct base
{
   virtual int operator()() { return 0; }
};
class derived: public base
{
   private:
      int i;
   public:
      derived(int ii)
      : i(ii) { }
      int operator()() { return i; }
};
int main()
{
   derived d(5);
   base* const b = &d;
   volatile int x = (*b)();
}
Compilando il codice con il g++3 con tutte le ottimizzazioni abilitate
ottengo il seguente codice assembler (a grandi line)
...
movl $_mem_fun + 8, un_registro
...
call *_mem_fun + 8
...
Se invece b non è const si ha (sempre a grandi linee)
call *un_registro
Tutto cio' mi lascia perplesso.
Non ne capisco molto di assembler, ma mi pare di capire che al momento
di chiamare la funzione virtuale il compilatore *sa esattamente* quale funzione
chiamare (infatti usa il nome della funzione e non un registro).
Se è così per quale motivo non espande in linea il codice della funzione?
E soprattutto, per quale motivo copia l'indirizzo della funzione da chiamere
in un registro?
Inoltre viene inserita anche tutta la parte riservata alla gestione delle
funzioni virtuali.
Forse ho mancato di riportare del codice importante, tuttavia l'unica call è
quella
riportata.
Saluti e grazie   Manlio Perillo

 [...]
In effetti ho ricontrollato meglio, quel _mem_fun è in realta un puntatore
alla virtual table..
Comunque mi chiedo ancora: quel const non serve a niente?
Il compilatore non può proprio sfruttare il fatto che quell'oggetto
non verrà più modificato?


non verrà più modificato?
I compilatori sono purtroppo a volte meno furbi di quanto
si potrebbe sperare. Il problema principale e' secondo me
che spesso gli ottimizzatori sono stati pensati per
agire sullo pseudo-codice generato da compilatori C e
non si avvantaggiano per nulla delle informazioni
aggiuntive provenienti dalla compilazione C++.
Inoltre l'equivalente codice C di una chiamata virtuale
(se basata su VMT) viene ad essere...
  (obj.vmt->my_method)(&obj,x);
e a nessuno verebbe in mente di lamentarsi che
l'ottimizzatore C non sfrutta il fatto che la VMT di
un oggetto non cambia mai e che se quindi l'oggetto
e' noto allora si sa quale deve essere la funzione
da chiamare (risparmiando l'indirezione).
Io ad esempio anche senza tirare in ballo metodi
virtuali ho visto che risulta difficile riuscire
ad ottenere anche una banale classe "punto 3d" che
sia efficiente quanto usare le tre coordinate in
modo esplicito... es:
  struct P3d
  {
    double x,y,z;
    P3d() {}
    P3d(double x,double y,double z)
    : x(x),y(y),z(z) { }
    P3d& operator+=(const P3d& p)
    { x+=p.x; y+=p.y; z+=p.z; return *this; }
  };
  inline P3d operator+(const P3d& a,const P3d& b)
  { return P3d(a)+=b; }
Questo frammento, pur essendo interamente dichiarato
inline (e quindi teoricamente completamente ottimizzabile)
porta alla generazione di codice molto meno efficiente
di quello che si potrebbe sperare. Anche inserendo
un po' di ridondanza ...
  inline P3d operator+(const P3d& a,const P3d& b)
  { return P3d(a.x+b.x,a.y+b.y,a.z+b.z); }
il codice migliora ma non raggiunge il livello
ottenibile scrivendo in C.
Forse questi problemi spariranno in futuro... o forse
no. Un mio ex-collega una volta ha detto "la buona
programmazione e' stata uccisa dai buoni editor"
riferendosi al copia-incolla per realizzare programmi...
una variante potrebbe essere "il buon software e' stato
ucciso dal buon hardware" ;)
Andrea

ottenibile scrivendo in C.
Che strano. Io tempo fa ebbi alcuni problemi di prestazioni con il
salvataggio su file di una quantità pari a 10000 record, tutti
memorizzati in un container membro della classe principale. Provai ad
usare la funzioni fwrite() del C al posto dei metodi di ofstream e non
riuscii a guadagnare più di un paio di decimi di secondo( considerando
che si trattava di 10000 record il guadagno era veramente scarso). Ho
sostituito tutto con un overload friend di operator<< ed ho guadagnato
più di 1 secondo e mezzo. Magari sarei riuscito a fare lo stesso
usando le funzioni del C ma avrei dovuto rinunciare all'information
hiding, ovvero avrei dovuto rendere pubblici i dati della classe.
Il Ghenio
---------
// TODO: Place your ass here

// TODO: Place your ass here
Andrea
// Ciucciati il calzino

"Andrea Griffini" ha scritto
: === Codice di test ===
:
:   //
:   // Io mi ero accorto del problema partendo da
:   // codice molto piu' complicato (e si trattava
:   // di codice che usava template per parametrizzare
:   // il tipo di coordinata). Semplificando e
:   // semplificando sono giunto a questa versione
:   // minimale...
:   //
:   struct P3d
:   {
:     double x,y,z;
:     P3d(double x,double y,double z)
:       : x(x),y(y),z(z)
:     {}
:   };
:
Ho aggiunto una mia versione di confronto, tutta C++, che sembra piu`
veloce di quella C;-) sia con g++ 3.0.2 su cygwin special, che con BCC
(foo sono due funzioni {} in un altro file). Magari analizzando anche
l'asm di questa riesci ad ottimizzare ancora di piu` la tua versione
ottimale.
HTH, ciao
Massimo
#include <ctime>
#include <iostream>
  //
  // Io mi ero accorto del problema partendo da
  // codice molto piu' complicato (e si trattava
  // di codice che usava template per parametrizzare
  // il tipo di coordinata). Semplificando e
  // semplificando sono giunto a questa versione
  // minimale...
  //
  struct P3d
  {
    double x,y,z;
    P3d(double x,double y,double z)
      : x(x),y(y),z(z)
    {}
    P3d(const P3d& a,const P3d& b)
      : x((a.x+b.x)/2), y((a.y+b.y)/2), z((a.z+b.z)/2) {}
  };
  inline P3d operator+(const P3d& a,const P3d& b)
  { return P3d(a.x+b.x,a.y+b.y,a.z+b.z); }
  inline P3d operator*(const P3d& a,double k)
  { return P3d(a.x*k,a.y*k,a.z*k); }
  // Un paio di punti in variabili globali
  P3d p0(1,2,3);
  P3d p1(4,5,6);
  // Stessa cosa in modo esplicito
  double x0=1,y0=2,z0=3;
  double x1=4,y1=5,z1=6;
  // Funzione esterna (non definita qui) che
  // riceve un punto (C++ e coordinate esplicite)
  void foo(double x,double y,double z);
  void foo(const P3d& p);
  // Chiama la funzione passando la media dei
  // due punti
  void bar1()
  {
    foo((p0+p1)*0.5);
  }
  // Idem, lavorando pero' esplicitamente per
  // coordinate
  void bar2()
  {
    foo((x0+x1)*0.5,
        (y0+y1)*0.5,
        (z0+z1)*0.5);
  }
  void bar3()
  {
    foo( P3d(p0, p1) );
  }
int main()
{
  std::clock_t t0 = std::clock();
  for (int j=0; j<10000000; ++j)
    bar1();
  std::cout << std::clock() - t0 << '\n';
  t0 = std::clock();
  for (int j=0; j<10000000; ++j)
    bar2();
  std::cout << std::clock() - t0 << '\n';
  t0 = std::clock();
  for (int j=0; j<10000000; ++j)
    bar3();
  std::cout << std::clock() - t0 << '\n';
  t0 = std::clock();
  for (int j=0; j<10000000; ++j)
    bar3();
  std::cout << std::clock() - t0 << '\n';
  t0 = std::clock();
  for (int j=0; j<10000000; ++j)
    bar2();
  std::cout << std::clock() - t0 << '\n';
  t0 = std::clock();
  for (int j=0; j<10000000; ++j)
    bar1();
  std::cout << std::clock() - t0 << '\n';
  std::cin.get();
}

ottimale.
Ho visto che BCC55 con la versione a "costruttore media" genera
codice molto "lineare" dal punto di vista istruzioni (senza copie
e fxch, che forse su PIII non servono ... non sono informato).
I tempi che ho rilevato sono comunque notevolmente migliori:
   bar1 = 15.164s    operator+
   bar2 = 11.181s    chiamata "C"
   bar3 = 6.554s     costruttore "media"
Con g++ (io sono ancora alla 2.95) il codice e' dello stesso
tipo (nessuna fxch) e piu' veloce nelle poche prove che ho
fatto sia della versione "C" che di quella "operator+".
Ho anche scoperto una cosa interessante... nella versione g++
se scrivo nel programma
  double x0=1,y0=2,z0=3;
  double x1=4,y1=5,z1=6;
ottengo un tempo (circa 7 secondi), se scrivo
  double x0=1,d1=1,y0=2,d2=1,z0=3,d3=1;
  double x1=4,d4=1,y1=5,d5=1,z1=6,d6=1;
inserendo delle variabili dummy che allineano
tutti quelle usate a 16 byte ne ottengo uno
nettamente migliore (4 secondi) e se scrivo
  double d0=1,x0=1,d1=1,y0=2,d2=1,z0=3,d3=1;
  double x1=4,d4=1,y1=5,d5=1,z1=6,d6=1;
"disallineando" tutte le variabili il tempo
sale a 12 secondi.
Questo mi fa pensare che sempre piu' i problemi di prestazioni
sono dovuti all'uso della RAM (e agli schemi di indirizzamento
e caching). In questo caso la differenza sul mio PIII/500 e'
un peggioramento del 300% su un loop di somme/moltiplicazioni
floating point semplicemente se si sbaglia l'indirizzo di
allocazione delle variabili coinvolte...
Certo e' che anche il tuo costruttore evidenzia che
l'ottimizzatore si comporta in modo sostanzialmente diverso
(generando programmi misurabilmente diversi) pur partendo da
codice che e' assolutamente equivalente da un punto di vista
puramente logico e anche fornendo tutte le possibilita' di
visione (tutto inline).
Posso quindi assumere che, con l'attuale tecnologia degli
ottimizzatori, il compilatore C++ fa sempre la cosa piu'
giusta ? Secondo me no... se non importa non importa... ma
se importa allora una guardatina vale la pena dargliela
Andrea