Hai bisogno di un gestore di memoria multi-threading

Dovrò creare un progetto multi-threading a breve che abbia visto esperimenti (delphitools.info/2011/10/13/memory-manager-investigations) che dimostrano che il gestore di memoria Delphi predefinito ha problemi con il multi-threading.

inserisci la descrizione dell'immagine qui

Quindi, ho trovato questo SynScaleMM. Qualcuno può dare un feedback su di esso o su un gestore di memoria simile?

Grazie

    Il nostro SynScaleMM è ancora sperimentale.

    EDIT: dai un’occhiata al più stabile ScaleMM2 e al nuovissimo SAPMM . Ma le mie osservazioni di seguito valgono ancora la pena: meno allocazioni, più scala!

    Ma ha funzionato come previsto in un ambiente server multi-thread. Lo scaling è molto meglio di FastMM4, per alcuni test critici.

    Ma il Memory Manager non è forse il collo di bottiglia più grande nelle applicazioni Multi-Threaded. FastMM4 potrebbe funzionare bene, se non lo sottolinei.

    Ecco alcuni consigli (non dogmatici, solo da esperimento e conoscenza di bassi livelli di Delphi RTL) se si desidera scrivere un’applicazione FAST multi-thread in Delphi:

    • Usa sempre const per parametri stringa o array dinamico come in MyFunc(const aString: String) per evitare di allocare una stringa temporanea per ogni chiamata;
    • Evitare l’uso della concatenazione di stringhe ( s := s+'Blabla'+IntToStr(i) ), ma fare affidamento su una scrittura bufferizzata come TStringBuilder disponibile nelle ultime versioni di Delphi;
    • TStringBuilder non è perfetto neanche: ad esempio, creerà molte stringhe temporanee per l’aggiunta di alcuni dati numerici e userà la funzione SysUtils.IntToStr() terribilmente lenta quando aggiungerai un valore integer – Dovevo riscrivere un sacco di bassi -levelocità per evitare la maggior parte delle allocazioni di stringhe nella nostra class TTextWriter come definito in SynCommons.pas ;
    • Non abusare delle sezioni critiche, lasciare che siano il più piccolo ansible, ma fare affidamento su alcuni modificatori atomici se è necessario un accesso concorrente – consultare ad esempio InterlockedIncrement / InterlockedExchangeAdd ;
    • InterlockedExchange (da SysUtils.pas) è un buon modo per aggiornare un buffer o un object condiviso. Si crea una versione aggiornata di alcuni contenuti nel thread, quindi si scambia un puntatore condiviso con i dati (ad esempio un’istanza TObject ) in un’operazione CPU di basso livello. Notificherà la modifica agli altri thread, con un ottimo ridimensionamento multi-thread. Dovrai occuparti dell’integrità dei dati, ma nella pratica funziona molto bene.
    • Non condividere dati tra thread, ma piuttosto creare la propria copia privata o fare affidamento su alcuni buffer di sola lettura (il pattern RCU è il migliore per il ridimensionamento);
    • Non utilizzare l’accesso indicizzato ai caratteri di stringa, ma fare affidamento su alcune funzioni ottimizzate come PosEx() per esempio;
    • Non mischiare il tipo di variabili / funzioni AnsiString/UnicodeString e controlla il codice asm generato tramite Alt-F2 per tracciare qualsiasi conversione nascosta indesiderata (es. call UStrFromPCharLen );
    • Piuttosto usa parametri var in una procedure invece di restituire una stringa (una funzione che restituisce una string aggiungerà una chiamata UStrAsg/LStrAsg che ha un LOCK che svuota tutti i core della CPU);
    • Se è ansible, per l’analisi dei dati o del testo, utilizzare i puntatori e alcuni buffer statici allocati allo stack anziché stringhe temporanee o array dinamici;
    • Non creare un TMemoryStream ogni volta che ne hai bisogno, ma affidarsi a un’istanza privata della class, già dimensionata in sufficiente memoria, nella quale scrivere i dati utilizzando la Position per recuperare la fine dei dati e non modificarne la Size (che essere il blocco di memoria assegnato dal MM);
    • Limitare il numero di istanze di class create: provare a riutilizzare la stessa istanza e, se ansible, utilizzare alcuni puntatori record/object su buffer di memoria già allocati, mappando i dati senza copiarli nella memoria temporanea;
    • Utilizza sempre lo sviluppo basato su test, con test multi-thread dedicato, cercando di raggiungere il limite del caso peggiore (aumentare il numero di thread, il contenuto dei dati, aggiungere dati incoerenti, mettere in pausa a caso, provare a stressare l’accesso alla rete o al disco, benchmark con tempistica su dati reali …);
    • Non fidarti mai del tuo istinto, ma usa un tempismo preciso su dati e processi reali.

    Ho cercato di seguire queste regole nel nostro framework Open Source e, se dai un’occhiata al nostro codice, scoprirai un sacco di codice di esempio del mondo reale.

    Se la tua app è compatibile con il codice GPL, ti consiglio Hoard . Dovrai scrivere il tuo wrapper, ma è molto semplice. Nei miei test, non ho trovato nulla che corrisponda a questo codice. Se il tuo codice non è in grado di ospitare la GPL, puoi ottenere una licenza commerciale di Hoard, a pagamento.

    Anche se non puoi utilizzare Hoard in una versione esterna del tuo codice, puoi confrontare le sue prestazioni con quelle di FastMM per determinare se la tua app ha problemi con la scalabilità di allocazione dell’heap.

    Ho anche scoperto che gli allocatori di memoria nelle versioni di msvcrt.dll distribuito con Windows Vista e successivamente scalano abbastanza bene sotto conflitto di thread, sicuramente molto meglio di FastMM. Io uso queste routine tramite il seguente Delphi MM.

     unit msvcrtMM; interface implementation type size_t = Cardinal; const msvcrtDLL = 'msvcrt.dll'; function malloc(Size: size_t): Pointer; cdecl; external msvcrtDLL; function realloc(P: Pointer; Size: size_t): Pointer; cdecl; external msvcrtDLL; procedure free(P: Pointer); cdecl; external msvcrtDLL; function GetMem(Size: Integer): Pointer; begin Result := malloc(size); end; function FreeMem(P: Pointer): Integer; begin free(P); Result := 0; end; function ReallocMem(P: Pointer; Size: Integer): Pointer; begin Result := realloc(P, Size); end; function AllocMem(Size: Cardinal): Pointer; begin Result := GetMem(Size); if Assigned(Result) then begin FillChar(Result^, Size, 0); end; end; function RegisterUnregisterExpectedMemoryLeak(P: Pointer): Boolean; begin Result := False; end; const MemoryManager: TMemoryManagerEx = ( GetMem: GetMem; FreeMem: FreeMem; ReallocMem: ReallocMem; AllocMem: AllocMem; RegisterExpectedMemoryLeak: RegisterUnregisterExpectedMemoryLeak; UnregisterExpectedMemoryLeak: RegisterUnregisterExpectedMemoryLeak ); initialization SetMemoryManager(MemoryManager); end. 

    Vale la pena ricordare che l’app deve martellare l’allocatore dell’heap piuttosto intensamente prima che il conflitto di thread in FastMM diventi un ostacolo alle prestazioni. Tipicamente nella mia esperienza ciò accade quando l’app esegue un sacco di elaborazione delle stringhe.

    Il mio consiglio principale per chiunque soffra di conflitto di thread sull’allocazione dell’heap consiste nel rielaborare il codice per evitare di colpire l’heap. Non solo eviti la contesa, ma eviti anche le spese per l’allocazione dell’heap: un twofer classico!

    Sta bloccando che fa la differenza!

    Ci sono due problemi da conoscere:

    1. Uso del prefisso LOCK dallo stesso Delphi (System.dcu);
    2. In che modo FastMM4 gestisce il conflitto dei thread e cosa fa dopo che non è riuscito ad acquisire un blocco.

    Uso del prefisso LOCK dallo stesso Delphi

    Borland Delphi 5, pubblicato nel 1999, è stato quello che ha introdotto il prefisso di lock nelle operazioni con le stringhe. Come sai, quando assegni una stringa a un’altra, non copia l’intera stringa ma semplicemente aumenta il contatore di riferimento all’interno della stringa. Se si modifica la stringa, si tratta di de-riferimenti, riduzione del contatore di riferimento e allocazione di spazio separato per la stringa modificata.

    In Delphi 4 e precedenti, le operazioni per aumentare e diminuire il contatore di riferimento erano normali operazioni di memoria. I programmatori che hanno usato Delphi lo sapevano e, e, se stavano usando le stringhe su thread, cioè passavano una stringa da un thread a un altro, hanno usato il proprio meccanismo di blocco solo per le stringhe rilevanti . I programmatori hanno anche utilizzato una copia stringa di sola lettura che non ha modificato in alcun modo la stringa sorgente e non ha richiesto il blocco, ad esempio:

     function AssignStringThreadSafe(const Src: string): string; var L: Integer; begin L := Length(Src); if L <= 0 then Result := '' else begin SetString(Result, nil, L); Move(PChar(Src)^, PChar(Result)^, L*SizeOf(Src[1])); end; end; 

    Ma in Delphi 5, Borland ha aggiunto il prefisso LOCK alle operazioni con le stringhe e sono diventati molto lenti, rispetto a Delphi 4, anche per le applicazioni a thread singolo.

    Per superare questa lentezza, i programmatori hanno iniziato a utilizzare i file patch SYSTEM.PAS "single threaded" con i commenti del blocco.

    Per ulteriori informazioni, consultare https://synopse.info/forum/viewtopic.php?id=57&p=1 .

    FastMM4 Conflitto di thread

    È ansible modificare il codice sorgente FastMM4 per un meccanismo di blocco migliore o utilizzare qualsiasi fork FastMM4 esistente, ad esempio https://github.com/maximmasiutin/FastMM4

    FastMM4 non è il più veloce per operazioni multicore, specialmente quando il numero di thread è maggiore del numero di socket fisici perché, di default, sulla contesa del thread (cioè quando un thread non può acquisire l'accesso ai dati, bloccato da un altro thread) chiama la funzione API di Windows Sleep (0), quindi, se il blocco non è ancora disponibile, entra in un ciclo chiamando Sleep (1) dopo ogni controllo del blocco.

    Ogni chiamata a Sleep (0) presenta il costo costoso di un interruttore di contesto, che può essere di oltre 100 cicli; inoltre subisce il costo dell'anello 3 per le transizioni dell'anello 0, che possono essere più di 1000 cicli. Per quanto riguarda Sleep (1) - oltre ai costi associati a Sleep (0) - ritarda anche l'esecuzione di almeno 1 millisecondo, cedendo il controllo ad altri thread e, se non ci sono thread in attesa di essere eseguiti da un core CPU fisico, mette il core in sleep, riducendo in modo efficace l'utilizzo della CPU e il consumo energetico.

    Ecco perché, su multithreded wotk con FastMM, l'utilizzo della CPU non ha mai raggiunto il 100% - a causa dello Sleep (1) emesso da FastMM4. Questo modo di acquisire serrature non è ottimale. Un modo migliore sarebbe stato uno spin-lock di circa 5000 istruzioni di pause e, se il blocco era ancora occupato, chiamando la chiamata API SwitchToThread (). Se la pause non è disponibile (su processori molto vecchi senza supporto SSE2) o SwitchToThread () la chiamata API non era disponibile (su versioni Windows precedenti, precedenti a Windows 2000), la soluzione migliore sarebbe utilizzare EnterCriticalSection / LeaveCriticalSection, che non La latenza è associata a Sleep (1) e che, in modo molto efficace, consente il controllo del core della CPU su altri thread.

    La fork che ho menzionato utilizza un nuovo approccio per l'attesa di un blocco, consigliato da Intel nel suo Manuale di ottimizzazione per gli sviluppatori: uno spinloop di pause + SwitchToThread () e, se uno di questi non è disponibile: CriticalSections anziché Sleep (). Con queste opzioni, lo Sleep () non verrà mai utilizzato ma verrà utilizzato EnterCriticalSection / LeaveCriticalSection. I test hanno dimostrato che l'approccio all'utilizzo di CriticalSections anziché Sleep (utilizzato in precedenza in FastMM4) fornisce un guadagno significativo in situazioni in cui il numero di thread che lavorano con il gestore della memoria è uguale o superiore al numero di core fisici. Il guadagno è ancora più evidente nei computer con più CPU fisiche e NUMA (Non-Uniform Memory Access). Ho implementato opzioni di compilazione per rimuovere l'approccio originale di FastMM4 dell'utilizzo di Sleep (InitialSleepTime) e quindi Sleep (AdditionalSleepTime) (o Sleep (0) e Sleep (1)) e sostituirli con EnterCriticalSection / LeaveCriticalSection per salvare preziosi cicli della CPU sprecato da Sleep (0) e per migliorare la velocità (ridurre la latenza) che è stata colpita ogni volta da almeno 1 millisecondo da Sleep (1), perché le sezioni critiche sono molto più compatibili con la CPU e hanno una latenza decisamente inferiore rispetto a Sleep (1) .

    Quando queste opzioni sono abilitate, FastMM4-AVX verifica: (1) se la CPU supporta SSE2 e quindi l'istruzione "pause" e (2) se il sistema operativo ha la chiamata API SwitchToThread () e, se entrambe le condizioni sono incontrato, usa "spin" loop-loop per 5000 iterazioni e poi SwitchToThread () invece di sezioni critiche; Se una CPU non ha l'istruzione "pausa" o Windows non ha la funzione API SwitchToThread (), utilizzerà EnterCriticalSection / LeaveCriticalSection.

    È ansible visualizzare i risultati del test, inclusi quelli creati su un computer con più CPU fisiche (socket) in tale fork.

    Vedi anche i loop di attesa di lunga durata di spin sull'articolo sui processori Intel abilitati alla tecnologia Hyper-Threading . Ecco ciò che Intel scrive su questo problema - e si applica molto bene a FastMM4:

    Il ciclo di attesa per la rotazione di lunga durata in questo modello di threading raramente causa un problema di prestazioni nei sistemi multiprocessore convenzionali. Ma può introdurre una penalità severa su un sistema con tecnologia Hyper-Threading perché le risorse del processore possono essere utilizzate dal thread master mentre è in attesa sui thread worker. Sleep (0) nel loop può sospendere l'esecuzione del thread master, ma solo quando tutti i processori disponibili sono stati presi dai thread worker durante l'intero periodo di attesa. Questa condizione richiede che tutti i thread di lavoro completino il loro lavoro contemporaneamente. In altre parole, i carichi di lavoro assegnati ai thread di lavoro devono essere bilanciati. Se uno dei thread di lavoro completa il proprio lavoro prima di altri e rilascia il processore, il thread principale può ancora essere eseguito su un processore.

    Su un sistema multiprocessore convenzionale ciò non causa problemi di prestazioni poiché nessun altro thread utilizza il processore. Ma su un sistema con tecnologia Hyper-Threading il processore su cui viene eseguito il master thread è logico e condivide le risorse del processore con uno degli altri thread di lavoro.

    La natura di molte applicazioni rende difficile garantire che i carichi di lavoro assegnati ai thread di lavoro siano bilanciati. Un'applicazione 3D multithread, ad esempio, può assegnare le attività per la trasformazione di un blocco di vertici dalle coordinate del mondo alle coordinate di visualizzazione di un gruppo di thread di lavoro. La quantità di lavoro per un thread di lavoro è determinata non solo dal numero di vertici ma anche dallo stato troncato del vertice, che non è prevedibile quando il thread master divide il carico di lavoro per i thread di lavoro.

    Un argomento diverso da zero nella funzione Sleep obbliga il thread in attesa a dormire N millisecondi, indipendentemente dalla disponibilità del processore. Potrebbe bloccare in modo efficace il thread in attesa dal consumare risorse del processore se il periodo di attesa è impostato correttamente. Ma se il periodo di attesa è imprevedibile dal carico di lavoro al carico di lavoro, allora un grande valore di N può rendere il thread in attesa troppo a lungo e un valore inferiore di N potrebbe farla svegliare troppo rapidamente.

    Pertanto, la soluzione preferita per evitare di sprecare risorse del processore in un ciclo di attesa di lunga durata è sostituire il ciclo con un'API di blocco del thread del sistema operativo, ad esempio API di threading di Microsoft Windows *, WaitForMultipleObjects. Questa chiamata fa sì che il sistema operativo blocchi il thread in attesa dal consumare risorse del processore.

    Si riferisce all'uso di Spin-Loops su processore Intel Pentium 4 e nota applicativa Processore Intel Xeon .

    Puoi anche trovare un'implementazione spin-loop molto buona qui su StackOverflow .

    Carica anche carichi normali solo per controllare prima di emettere un archivio lock , solo per non inondare la CPU con operazioni bloccate in un ciclo, che bloccherebbe il bus.

    FastMM4 di per sé è molto buono. Basta migliorare il blocco e otterrete un gestore di memoria multi-threaded eccellente.

    Si noti inoltre che ciascun tipo di blocco di piccole dimensioni è bloccato separatamente in FastMM4.

    È ansible inserire il padding tra le aree di controllo dei blocchi piccoli, in modo che ciascuna area abbia una propria linea di cache, non condivisa con altre dimensioni di blocco e per assicurarsi che inizi con un limite di dimensione della linea della cache. È ansible utilizzare CPUID per determinare la dimensione della linea della cache della CPU.

    Quindi, con il blocco implementato correttamente per soddisfare le tue esigenze (ad esempio se hai bisogno di NUMA o no, se usare i rilasci di lock -ing, ecc., Potresti ottenere i risultati che le routine di allocazione della memoria sarebbero diverse volte più veloci e non ne soffrirebbero severamente dalla contesa del thread.

    FastMM si occupa di multi-threading bene. È il gestore di memoria predefinito per Delphi 2006 e versioni successive.

    Se si utilizza una versione precedente di Delphi (Delphi 5 e versioni successive), è comunque ansible utilizzare FastMM. È disponibile su SourceForge .

    È ansible utilizzare TopMM: http://www.topsoftwaresite.nl/

    Si potrebbe anche provare ScaleMM2 ( SynScaleMM è basato su ScaleMM1) ma devo correggere un bug riguardante la memoria di interfread, quindi non ancora pronto per la produzione 🙁 http://code.google.com/p/scalemm/

    Deplhi 6 memory manager è obsoleto e completamente negativo. Stavamo usando RecyclerMM sia su un server di produzione ad alto carico che su un’applicazione desktop multi-thread e non abbiamo riscontrato problemi: è veloce, affidabile e non causa un’eccessiva frammentazione. (La frammentazione era il peggior problema della memoria di magazzino di Delphi).

    L’unico inconveniente di RecyclerMM è che non è compatibile con MemCheck out of the box. Tuttavia, una piccola modifica alla fonte era sufficiente a renderla compatibile.