PDA

View Full Version : [C++] Override di new e custom allocators


Tommo
06-11-2008, 15:43
Salve,
prima di tutto volevo chiedere consiglio su Nedmalloc (http://www.nedprod.com/programs/portable/nedmalloc/), un malloc custom scritto da non so chi... lo ha consigliato il founder di Ogre3D e in effetti è capace di una pazza velocità pari a 125x il malloc di Windows, provato con un test sulla mia macchina (20.000.000 di malloc free di doubles) :sofico:
In sostanza, perchè non lo usano tutti? E dove sta la fregatura? :asd:

Cmq, dopo aver scoperto questo coso, mi sono posto il problema di rimpiazzare tutti i new, alloc, malloc, delete eccetera del mio framework con i ned-corrispettivi... solo che mi sono scontrato con tutti quanti i limiti del C++!

Dopo parecchia fatica sono riuscito a creare una classe e uno schifo statico che mi permettono di scegliere a runtime l'interfaccia della memoria, ma non sono ancora soddisfatto, anche se funziona:

Ecco il codice:

Interfaccia:

#ifndef MEMORY_ALLOCATOR_INTERFACE_H
#define MEMORY_ALLOCATOR_INTERFACE_H

namespace eVolve
{
namespace Core
{
///Interface class to be used to have a custom memory allocator
/**
See also the GameServer constructor.
*/
class EVOLVEEXPORT MemoryAllocatorInterface
{
public:
///pure allocator
virtual void* malloc(unsigned int size)=0;

virtual void* realloc(void* memory, unsigned int size)=0;

///new call (allocates and then the runtime calls constructor
virtual void* instance(unsigned int size)=0;

///delete + free call
virtual void free(void* memory)=0;
};
}
}

#endif


Wrapper per renderne l'utilizzo uguale agli allocatori standard:

#ifndef MEMORY_ALLOCATOR_WRAPPER_H
#define MEMORY_ALLOCATOR_WRAPPER_H

#include "eVolve_Conf.h"

#ifndef PREVENT_CUSTOM_MEMORY_ALLOCATORS

#include "MemoryAllocatorInterface.h"

//give some pretty names xD
#define EVOLVE_MALLOC eVolve_malloc
#define EVOLVE_REALLOC eVolve_realloc
#define EVOLVE_NEW new
#define EVOLVE_DELETE delete

namespace eVolve
{
namespace Core
{
class MemoryAllocatorInterface;
class EVOLVEEXPORT MemoryAllocatorWrapper
{
public:

//Be careful! Changing this at runtime can cause any problem!
static void _setMemoryAllocator( MemoryAllocatorInterface* malloc )
{
alloc = malloc;
}

static MemoryAllocatorInterface* _getMemoryAllocator() { return alloc; }

private:
static MemoryAllocatorInterface* alloc;
};

}
}

///////pure allocator
inline void* eVolve_malloc(unsigned int size)
{
return eVolve::Core::MemoryAllocatorWrapper::_getMemoryAllocator()->malloc( size );
}

inline void* eVolve_realloc(void* memory, unsigned int size)
{
return eVolve::Core::MemoryAllocatorWrapper::_getMemoryAllocator()->realloc( memory,size );
}
///mono-new
inline void* operator new(unsigned int size)
{
return eVolve::Core::MemoryAllocatorWrapper::_getMemoryAllocator()->instance(size);
}

///array new
inline void* operator new[](unsigned int size)
{
return eVolve::Core::MemoryAllocatorWrapper::_getMemoryAllocator()->instance(size);
}

///mono-EVOLVE_DELETE
inline void operator EVOLVE_DELETE(void* memory)
{
eVolve::Core::MemoryAllocatorWrapper::_getMemoryAllocator()->free(memory);
}

///array EVOLVE_DELETE
inline void operator EVOLVE_DELETE[](void* memory)
{
eVolve::Core::MemoryAllocatorWrapper::_getMemoryAllocator()->free(memory);
}

#endif
#endif


Dopo tutta questa roba, se setto l'interfaccia di memoria ed uso gli EVOLVE_qualcosa tutto va liscio... e guadagno anche 3 o 4 frames al secondo...
tuttavia il problema è che il C++ mi obbliga a dichiarare new nel global namespace, ma così il new originale va perso!
In sostanza, io voglio usare il custom allocator SOLO quando lo chiamo esplicitamente, e voglio lasciare new al suo posto... ma voglio anche che EVOLVE_NEW abbia le stesse prerogative, come la sintassi senza parentesi, l'autocast e la chiamata al costruttore.

In più la classe wrapper è bruttina, è quanto di meno OOP possa esistere :asd:
Conscio dell'orrendezza, mi sapreste consigliare qualcosa per rimediare a questi problemi?

marco.r
06-11-2008, 17:55
due idee.
1 - Se l'uso della malloc standard piuttosto che quella "furba" lo vuoi fare in base alla classe, allora ti conviene definire l'operatore new in classe base e poi derivarla

struct CustomAllocated
{
static void* operator new (size_t size)
{
// ritorna memoria allocata con l'allocatore custom
}

static void operator delete(void* p)
{
// libera la memoria con la corrispondente free
}
};


class Foo: public CustomAllocated
{
/* ... */
};


Cosi' ti basta chiamare

Foo* x = new Foo();

per usare il nuovo allocatore

marco.r
06-11-2008, 18:08
Se vuoi decidere a runtime quale usare il discorso e' piu' complicato, ma si puo' comunque fare.
Un esempio semplice e'


struct CA {};
CA ca;

void* operator new (size_t size, CA )
{
return my_malloc(size);
}


void operator delete(void* p, CA )
{
my_free(p);
}

In questo caso se fai

int* x = new int();

Usi l'allocatore classico mentre con

int* x = new (ca) int();

Usi l'allocatore custom. Il problema resta che devi dichiarare esplicitamente che vuoi usare il distruttore relativo, ovvero devi sapere al momento della distruzione come era stato allocato l'oggetto.
Puoi anche fare cose piu' evolute, come fare piu' allocatori specifici per una classe, oppure passare un oggetto non idiota come argomento della new, ad esempio l'arena da cui attingere memoria etc.

Tommo
06-11-2008, 21:10
Gegno :D

il secondo approccio è quello che fa per me, dato che già uso il mio bravo define... basta overloadare con un bel bool, anche se temo che questo crei un minimo di overhead sul new normale:


#define EVOLVE_NEW new(true)
#define EVOLVE_DELETE delete(true)


E poi stiano attenti gli user a chiamare il delete giusto :asd:
Che poi, ho spesso usato malloc con delete, nedalloc con free, e non cambia niente nel 90% dei casi...
Provo e riferisco :D

EDIT:

Provato, e new funge :D
Tuttavia delete in quella maniera non va, mi dice "impossibile cancellare un parametro che non è pointer", riferendosi sicuramente al bool.

Tuttavia la sintassi dovrebbe essere "delete(true) memory" no?

EDIT2:

Ok, tagliato la testa al C++: ora il custom delete non è più un operatore ma una funzione normale.
Il corrispettivo ad EVOLVE_NEW mem sarà quindi EVOLVE_DELETE(mem).
Tanto delete è banale e già assomiglia ad una funzione :sofico:

71104
07-11-2008, 10:57
In sostanza, perchè non lo usano tutti? E dove sta la fregatura? :asd: hai controllato l'effettiva occupazione di memoria virtuale? la... "malloc di Windows" come la chiami tu, è più lenta perché non chiama direttamente HeapAlloc, ma usa un meccanismo che riduce la frammentazione della memoria virtuale. prova a fare un benchmark in cui allochi una marea di blocchi delle dimensioni più svariate e le allocazioni sono "interleaved" da deallocazioni: secondo me la "malloc di Windows" darà risultati migliori in termini di occupazione di memoria.

Tommo
07-11-2008, 14:21
hai controllato l'effettiva occupazione di memoria virtuale? la... "malloc di Windows" come la chiami tu, è più lenta perché non chiama direttamente HeapAlloc, ma usa un meccanismo che riduce la frammentazione della memoria virtuale. prova a fare un benchmark in cui allochi una marea di blocchi delle dimensioni più svariate e le allocazioni sono "interleaved" da deallocazioni: secondo me la "malloc di Windows" darà risultati migliori in termini di occupazione di memoria.

Beh, l'occupazione di memoria non mi interessa nemmeno saperla... da quello che ho visto arrivo a fatica a 70mb, e date le capacità di oggi, se occupare il doppio facesse guadagnare il 6% in velocità sarebbe lo stesso conveniente.
E qui si parla di vantaggi molto maggiori, sulla carta.

In ogni caso, se avessi letto prima l'homepage di quell'allocator, avresti visto che è più veloce proprio perchè usa un meccanismo di deframmentazione migliore di quello del malloc di win32. Che si, è diverso da quello di Linux.

Peccato che non riesca a provare, perchè Lua mi va in heap corruption e non ho idea del perchè :stordita:

71104
07-11-2008, 15:00
Beh, l'occupazione di memoria non mi interessa nemmeno saperla... da quello che ho visto arrivo a fatica a 70mb, e date le capacità di oggi, se occupare il doppio facesse guadagnare il 6% in velocità sarebbe lo stesso conveniente. ma manco per sogno: se superi il limite massimo per il working set vai in swap, e lo swap ammazza le prestazioni come nient'altro. non importa che la macchina abbia 2 gigabyte e mezzo di RAM fisica: il working set ha un limite massimo molto inferiore (1 megabyte e mezzo di default mi sembra).
inoltre se non ti interessa l'occupazione di memoria è inutile che usi malloc: usa HeapAlloc e otterrai una velocità maggiore di qualunque malloc, per definizione.

[...] del malloc di win32. Win32 non ha nessuna malloc. la malloc a cui ti riferisci tu è quella del runtime di Visual C++.

Tommo
07-11-2008, 15:22
ma manco per sogno: se superi il limite massimo per il working set vai in swap, e lo swap ammazza le prestazioni come nient'altro. non importa che la macchina abbia 2 gigabyte e mezzo di RAM fisica: il working set ha un limite massimo molto inferiore (1 megabyte e mezzo di default mi sembra).
inoltre se non ti interessa l'occupazione di memoria è inutile che usi malloc: usa HeapAlloc e otterrai una velocità maggiore di qualunque malloc, per definizione.

Win32 non ha nessuna malloc. la malloc a cui ti riferisci tu è quella del runtime di Visual C++.

Boh, io ho preso per buono sia il test (che lo dava 125x volte più veloce su ventimila malloc/free di 1000 double ciascuno)... test che conferma fra l'altro i valori dichiarati sul loro sito... quindi avevo deciso di fidarmi :asd:

Cmq grazie per la chiarificazione sul "malloc di win32", per quanto ormai se si programma per win32 VC++ ha il dominio assoluto.

Domandine:
ma Windows che ruolo ha nell'allocazione della memoria? Di certo non lascia fare come vuole al programma.

E, memcpy, memcmp, memmove e simili sono compatibili con tutti gli allocators o solamente con ::malloc e ::free?

Grazzie mille :stordita:

marco.r
07-11-2008, 15:46
Gegno :D

il secondo approccio è quello che fa per me, dato che già uso il mio bravo define... basta overloadare con un bel bool, anche se temo che questo crei un minimo di overhead sul new normale:


#define EVOLVE_NEW new(true)
#define EVOLVE_DELETE delete(true)


Invece che usare un bool usa struct diverse, come proponevo sopra, e non hai alcun overhead (quale new chiamare viene risolto durante la compilazione). Anzi una struct sola visto che se non specifichi nulla continui ad usare la new standard.


E poi stiano attenti gli user a chiamare il delete giusto :asd:
Che poi, ho spesso usato malloc con delete, nedalloc con free, e non cambia niente nel 90% dei casi...
Provo e riferisco :D

Non mi sembra sia garantito dallo standard che new/delete usino malloc/free, quindi probabilmente e' una scelta poco portabile. Se poi usi una classe scritta da altri e non ti accorgi che ha ridefinito l'operatore new son cavoli. Oltre al fatto, ovviamente, che con la malloc/free devi chiamare esplicitamente costruttore e distruttore di una classe.

Tuttavia la sintassi dovrebbe essere "delete(true) memory" no?

Per qualche misterioso motivo no, anzi la sintassi e' particolarmente scomoda, qualcosa tipo

Foo* x = ...
operator delete (x,true);

o giu' di li'.

Tommo
07-11-2008, 16:10
Quindi non c'è modo di cavarsela col define come new... vabè tanto delete sta bene come funzione.

Cmq sto in fase di rosik perchè finalmente funziona tutto ma... il vantaggio di fps è davvero aleatorio!
Viaggia sui 190 quando il default sta a 200, mentre sta sugli 80 quando il default oscilla fra 40 e 70. In debug però ha la cattva abitudine di far frullare il disco, e rende l'applicazione più laggosa... almeno dai primi test che ho fatto.

In release invece va una crema, rimanendo fisso a 62 fps con qualsiasi scena, dato che c'è il FPS lock a 60. Ma lì il default faceva 60... tuttavia col primo sembra essere leggermente più scorrevole.

Conclusioni: è stato molto educativa come cosa, ma il vantaggio in applicazioni che non smuovono tonnellate di memoria è praticamente nullo. Al massimo ho 1 fps di più, un pò di responsività e 0.5 s di meno durante il loading.
Tutta la velocità se la mangiano allegramente la grafica e i calcoli di gioco...

In finale, pare fatto apposta per i contests :asd:

71104
07-11-2008, 19:49
ma Windows che ruolo ha nell'allocazione della memoria? Di certo non lascia fare come vuole al programma. il subsystem Win32 prevede fondamentalmente due set di API da questo punto di vista: le API della memoria virtuale (VirtualXxx) che permettono di gestire allocazioni di memoria a livello di pagine, e le Heap API (HeapXxx) che si basano sul set precedente ed introducono un semplice framework per la gestione di heap multipli in cui puoi allocare blocchi di memoria la cui dimensione non sia necessariamente un multiplo della dimensione di una pagina. le Heap API introducono già per conto loro un qualche sistema di ottimizzazione: chiaramente non è che ad ogni HeapAlloc corrisponde una VirtualAlloc, altrimenti sprecheresti quasi sempre dei bytes.

quello che non mi sembra ti sia chiaro è che la malloc non è una funzione API Win32 offerta dal sistema operativo, ma è una funzione del runtime dello specifico compilatore: di HeapAlloc ce n'è una su tutto Windows, di malloc ce n'è una per ogni compilatore.*
in generale considera che un programma Win32 non ha nessun modo di allocare dinamicamente memoria al di fuori di VirtualAlloc(Ex) e HeapAlloc, il che significa che qualunque compilatore (incluso quello Microsoft) deve per forza offrire una implementazione di malloc basata su uno di quei due set di API. in generale le malloc sono basate su HeapAlloc, e per questo motivo io prima ti dicevo che se vuoi il massimo della velocità e non ti interessa l'occupazione di memoria basta che usi HeapAlloc (e magari allarghi il limite del working set :D), poi però mi è venuto in mente che se questa nedmalloc è così veloce forse è perché bypassa le Heap API e usa direttamente la VirtualAlloc praticando le sue ottimizzazioni.

*fatta eccezione per i compilatori che decidono comunque di usare la malloc del (ed in generale tutto il) runtime di Visual C++; ciò è possibile grazie al fatto che tale runtime è distribuito con Windows e quindi è presente su tutte le macchine. può capitare a volte che un programma richieda una versione del runtime di Visual C++ non presente sulla macchina, in tal caso il programma deve essere distribuito assieme al redistributable della versione di Visual C++ con cui è stato scritto.


E, memcpy, memcmp, memmove e simili sono compatibili con tutti gli allocators o solamente con ::malloc e ::free? sono compatibili con tutti gli allocatori: quelle che citi sono routines che lavorano sui bytes senza dover leggere gli eventuali headers che lo specifico allocatore mette in testa al blocco per gestire la riallocazione e la deallocazione.
tra l'altro quelle routines non si debbono per forza usare su blocchi di memoria allocati nell'heap: se le usi su un array allocato sullo stack o nel segmento dati non fanno una piega, overflow/underflow a parte.

71104
07-11-2008, 20:10
Cmq sto in fase di rosik perchè finalmente funziona tutto ma... il vantaggio di fps è davvero aleatorio!
Viaggia sui 190 quando il default sta a 200, mentre sta sugli 80 quando il default oscilla fra 40 e 70. In debug però ha la cattva abitudine di far frullare il disco, e rende l'applicazione più laggosa... almeno dai primi test che ho fatto. azzardo la mia: questo forse perché fai la solita confusione tra ottimizzazione del codice C o C++ e velocità generale del programma. le due cose sarebbero strettamente legate se non si trattasse di un'applicazione grafica, come invece ho capito che è: le applicazioni grafiche caricano la GPU, non la CPU (o meglio, in genere caricano molto di più la GPU); il vantaggio di una malloc veloce non lo puoi certo misurare in fps!


Tutta la velocità se la mangiano allegramente la grafica e i calcoli di gioco... appunto...

Tommo
07-11-2008, 21:23
azzardo la mia: questo forse perché fai la solita confusione tra ottimizzazione del codice C o C++ e velocità generale del programma. le due cose sarebbero strettamente legate se non si trattasse di un'applicazione grafica, come invece ho capito che è: le applicazioni grafiche caricano la GPU, non la CPU (o meglio, in genere caricano molto di più la GPU); il vantaggio di una malloc veloce non lo puoi certo misurare in fps!


appunto...

grazie mille sei stato chiarissimo, in effetti non avevo ben presente la diffrenza fra le due. E' sempre difficilissimo trovare referenze su queste cose più approfondite in giro. :D
Cmq non me la sento di usare HeapAlloc, mi pare un tantino troppo di basso livello, per quanto basterebbe implementare la classe interfaccia per usarlo :asd:

Cmq avevo già fatto il ragionamento della grafica, ma mi aspettavo comunque vantaggi percepibili nel mio gioco (nel link in firma): dato che raggiunge agevolmente i 280fps sul mio pc, non è sicuramente GPU limited... il grosso del peso lo fanno la routine di creazione degli oggetti, l'ai dei tanti omini, e PhysX.
Tuttavia son tutti algoritmi che incidono pochissimo sulla memoria, per cui il tempo di allocazione è trascurabile.

Certo, se deciderò di fare GTA V lo troverò di certo più utile :asd:

Tommo
08-11-2008, 15:31
Upperello...

Tutto sembrava funzionare, ma sono iniziati a sorgere strani bugs:


inline void operator delete(void* memory, bool)
{
eVolve::Core::MemoryAllocatorWrapper::_getMemoryAllocator()->free(memory,false);
}

#define EVOLVE_DELETE(T) operator delete (T,true)

EVOLVE_DELETE(mem);



Il seguente codice dovrebbe essere in grado di definire un delete overloaded, rinominarlo come EVOLVE_DELETE e poi chiamarlo.
In effetti lo fa, ma manca tutto il "contorno"!
Cioè, non chiama il distruttore sugli oggetti che va ad deallocare, causando ogni genere di bugs.

Suppongo che sia perchè chiamando direttamente "operator delete" si impedisce alla runtime di gestirsi tutti i suoi meccanismi nascosti... come faccio allora a chiamare esplicitamente questo maledetto distruttore?
A sto punto voglio vederla finita sta cosa :asd:

EDIT: Per la precisione, non chiama i distruttori della classe Super, ma solo l'ultimo. A volte. Doh :doh:

Tommo
10-11-2008, 16:17
Up.. nessuna idea?
Fortuna che ho implementato l'autodistruzione: #define PREVENT_CUSTOM_MEMORY_ALLOCATORS che fa tornare tutto come prima:doh:

marco.r
13-11-2008, 09:51
Non ho mai avuto necessita' pratica di usare allocatori diversi per una stessa classe, (a meno che non fosse una arena) per cui vado a spanne; mi sembra comunque che chiamate esplicite a operator delete semplicemente chiamino la funzione. Se hai sottomano lo Stroustrup se non ricordo male c'e' una sezione a riguardo, pure abbastanza dettagliata, che dovrebbe illuminarti.