Chaînes de traitement en parallèle Delphi plein usage CPU disponible
Le but est d'obtenir une utilisation complète des noyaux disponibles, en convertissant les flotteurs en chaînes dans une seule application Delphi. Je pense que ce problème s'applique au traitement général de la chaîne. Pourtant, dans mon exemple, j'utilise spécifiquement la méthode FloatToStr.
ce que je fais (j'ai gardé cela très simple donc il y a peu d'ambiguïté autour de la mise en oeuvre):
- à l'Aide de Delphi XE6
- créer des objets thread qui héritent de TThread, et démarrer ils.
- dans la procédure d'exécution du thread, il convertira une grande quantité de double en cordes via la méthode FloatToStr.
- pour simplifier, ces doubles sont juste la même constante, donc il n'y a pas ressource mémoire partagée ou globale requise par les threads.
bien que plusieurs noyaux soient utilisés, le % D'utilisation CPU sera toujours max sur la quantité d'un seul noyau. Je comprends que c'est un problème. J'ai donc quelques spécifiques question.
d'une manière simple, la même opération pourrait être effectuée par plusieurs instances app, et ainsi obtenir une utilisation plus complète du CPU disponible. Est-il possible de le faire efficacement dans le même exécutable ? C'est-à-dire: attribuer des threads différents identifiants de processus au niveau de L'OS ou une division équivalente reconnue par L'OS ? Ou est-ce tout simplement pas possible dans hors de la boîte Delphi ?
Sur le champ d'application : Je sais qu'il existe différents gestionnaires de mémoire disponibles et d'autres groupes ont j'ai essayé de changer certains des niveaux inférieurs de l'utilisation des serrures asmhttp://synopse.info/forum/viewtopic.php?id=57 Mais je pose cette question dans le but de ne pas faire les choses à un niveau aussi bas.
Merci
<!-Mon code est délibérément très simple:
TTaskThread = class(TThread)
public
procedure Execute; override;
end;
procedure TTaskThread.Execute;
var
i: integer;
begin
Self.FreeOnTerminate := True;
for i := 0 to 1000000000 do
FloatToStr(i*1.31234);
end;
procedure TfrmMain.Button1Click(Sender: TObject);
var
t1, t2, t3: TTaskThread;
begin
t1 := TTaskThread.Create(True);
t2 := TTaskThread.Create(True);
t3 := TTaskThread.Create(True);
t1.Start;
t2.Start;
t3.Start;
end;
il s'agit d'un' code test', où le CPU (via performance monitor) sort à 25% (j'ai 4 cœurs). Si la ligne FloatToStr est changée pour une opération sans chaîne, par exemple Puissance(i, 2), puis le moniteur de performance montre l'utilisation attendue de 75%. (Oui, il y a de meilleures façons de le mesurer, mais je pense que c'est suffisant pour la portée de cette question)
j'ai exploré cette question assez à fond. Le but de la question était de mettre en avant le nœud du problème sous une forme très simple.
je pose des questions sur les limitations lors de l'utilisation de la méthode FloatToStr. Et se demander s'il y a une incarnation de mise en œuvre qui permettra une meilleure utilisation de les cœurs disponibles.
Merci.
5 réponses
je seconde ce que tout le monde l'a dit dans les commentaires. C'est l'un des petits secrets de Delphi que le Gestionnaire de mémoire FastMM n'est pas extensible.
puisque les gestionnaires de mémoire peuvent être remplacés, vous pouvez simplement remplacer FastMM par un gestionnaire de mémoire évolutif. C'est un domaine en rapide évolution. De nouveaux gestionnaires de mémoire évolutifs apparaissent tous les quelques mois. Le problème est qu'il est difficile d'écrire un bon évolutive gestionnaire de mémoire. De quoi êtes-vous prêt à faire confiance? Une chose qui peut être dit en faveur de FastMM est qu'il est robuste.
Plutôt que de remplacer le gestionnaire de mémoire, il est préférable de remplacer la nécessité de remplacer le gestionnaire de mémoire. Il suffit d'éviter l'allocation de tas. Trouvez un moyen de faire votre travail avec le besoin d'appels répétés pour allouer la mémoire dynamique. Même si vous aviez un gestionnaire de tas extensible, l'allocation de tas coûterait quand même.
une fois que vous avez décidé d'éviter l'allocation de tas, la prochaine décision est ce qu'il faut utiliser au lieu de FloatToStr
. D'après mon expérience, le Delphi la bibliothèque runtime n'offre pas beaucoup de support. Par exemple, j'ai récemment découvert qu'il n'y a pas de bonne façon de convertir un entier en texte en utilisant un tampon fourni par l'appelant. Ainsi, vous pouvez avoir besoin de lancer vos propres fonctions de conversion. Comme une première étape simple pour prouver le point, essayez d'appeler sprintf
msvcrt.dll
. Cela fournira une preuve de concept.
Si vous ne pouvez pas changer le gestionnaire de mémoire (MM) la seule chose à faire est d'éviter de l'utiliser où MM pourrait être un goulot d'étranglement.
comme pour la conversion flottante en chaîne (Dislamer: j'ai testé le code ci-dessous avec Delphi XE) au lieu de
procedure Test1;
var
i: integer;
S: string;
begin
for i := 0 to 10 do begin
S:= FloatToStr(i*1.31234);
Writeln(S);
end;
end;
vous pouvez utiliser
procedure Test2;
var
i: integer;
S: string;
Value: Extended;
begin
SetLength(S, 64);
for i := 0 to 10 do begin
Value:= i*1.31234;
FillChar(PChar(S)^, 64, 0);
FloatToText(PChar(S), Value, fvExtended, ffGeneral, 15, 0);
Writeln(S);
end;
end;
qui produisent le même résultat mais n'allouent pas de mémoire à l'intérieur de la boucle.
Et de prendre à l'attention de
function FloatToStr(Value: Extended): string; overload;
function FloatToStr(Value: Extended; const FormatSettings: TFormatSettings): string; overload;
La première forme de FloatToStr n'est pas thread-safe, car elle utilise des informations de localisation contenues dans des variables globales. La deuxième forme de FloatToStr, qui est thread-safe, se réfère aux informations de localisation contenues dans le paramètre FormatSettings. Avant d'appeler la forme thread-safe de FloatToStr, vous devez remplir les FormatSettings d'informations de localisation. Pour remplir les FormatSettings avec un ensemble de valeurs locales par défaut, appelez GetLocaleFormatSettings.
merci Beaucoup pour vos connaissances et votre aide pour l'instant. Conformément à vos suggestions, j'ai essayé d'écrire une méthode FloatToStr équivalente d'une manière qui évite l'allocation de tas. Pour un certain succès. Il ne s'agit en aucun cas d'une mise en œuvre à l'épreuve, mais d'une validation de principe simple et agréable qui pourrait être étendue pour obtenir une solution plus satisfaisante.
(faut également noter à l'aide de XE6 64 bits)
résultat de L'expérience / observations:
- l'utilisation de l'UC % était proportionnel au nombre de threads lancés (c'est à dire chaque thread = 1 core maxed via l'analyseur de performances).
- comme prévu, avec plus de threads lancés, les performances se sont quelque peu dégradées pour chaque individu (c. - à-d. le temps mesuré pour effectuer la tâche-voir le code).
les temps ne sont que des moyennes approximatives
- 8 cœurs 3,3 GHz-1 fil a pris 4200ms. 6 threads ont pris 5200ms chacun.
- 8 cœurs 2,5 GHz-1 fil a pris 4800ms. 2= > 4800ms, 4=>5000ms, 6= > 6300M.
Je n'ai pas calculé le temps total pour une exécution multi-thread. Je viens d'observer le pourcentage D'utilisation du processeur et de mesurer les temps de thread individuels.
personnellement je trouve un peu hilarant que cela fonctionne réellement :) ou peut-être j'ai fait quelque chose d'horriblement mal ?
sûrement il y a des unités de bibliothèque là-bas qui résolvent ces choses ?
le code:
unit Main;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
Generics.Collections,
DateUtils;
type
TfrmParallel = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
TTaskThread = class(TThread)
private
Fl: TList<double>;
public
procedure Add(l: TList<double>);
procedure Execute; override;
end;
var
frmParallel: TfrmParallel;
implementation
{$R *.dfm}
{ TTaskThread }
procedure TTaskThread.Add(l: TList<double>);
begin
Fl := l;
end;
procedure TTaskThread.Execute;
var
i, j: integer;
s, xs: shortstring;
FR: TFloatRec;
V: double;
Precision, D: integer;
ZeroCount: integer;
Start, Finish: TDateTime;
procedure AppendByteToString(var Result: shortstring; const B: Byte);
const
A1 = '1';
A2 = '2';
A3 = '3';
A4 = '4';
A5 = '5';
A6 = '6';
A7 = '7';
A8 = '8';
A9 = '9';
A0 = '0';
begin
if B = 49 then
Result := Result + A1
else if B = 50 then
Result := Result + A2
else if B = 51 then
Result := Result + A3
else if B = 52 then
Result := Result + A4
else if B = 53 then
Result := Result + A5
else if B = 54 then
Result := Result + A6
else if B = 55 then
Result := Result + A7
else if B = 56 then
Result := Result + A8
else if B = 57 then
Result := Result + A9
else
Result := Result + A0;
end;
procedure AppendDP(var Result: shortstring);
begin
Result := Result + '.';
end;
begin
Precision := 9;
D := 1000;
Self.FreeOnTerminate := True;
//
Start := Now;
for i := 0 to Fl.Count - 1 do
begin
V := Fl[i];
// //orignal way - just for testing
// xs := shortstring(FloatToStrF(V, TFloatFormat.ffGeneral, Precision, D));
//1. get float rec
FloatToDecimal(FR, V, TFloatValue.fvExtended, Precision, D);
//2. check sign
if FR.Negative then
s := '-'
else
s := '';
//2. handle negative exponent
if FR.Exponent < 1 then
begin
AppendByteToString(s, 0);
AppendDP(s);
for j := 1 to Abs(FR.Exponent) do
AppendByteToString(s, 0);
end;
//3. count consecutive zeroes
ZeroCount := 0;
for j := Precision - 1 downto 0 do
begin
if (FR.Digits[j] > 48) and (FR.Digits[j] < 58) then
Break;
Inc(ZeroCount);
end;
//4. build string
for j := 0 to Length(FR.Digits) - 1 do
begin
if j = Precision then
Break;
//cut off where there are only zeroes left up to precision
if (j + ZeroCount) = Precision then
Break;
//insert decimal point - for positive exponent
if (FR.Exponent > 0) and (j = FR.Exponent) then
AppendDP(s);
//append next digit
AppendByteToString(s, FR.Digits[j]);
end;
// //use just to test agreement with FloatToStrF
// if s <> xs then
// frmParallel.Memo1.Lines.Add(string(s + '|' + xs));
end;
Fl.Free;
Finish := Now;
//
frmParallel.Memo1.Lines.Add(IntToStr(MillisecondsBetween(Start, Finish)));
//!YES LINE IS NOT THREAD SAFE!
end;
procedure TfrmParallel.Button1Click(Sender: TObject);
var
i: integer;
t: TTaskThread;
l: TList<double>;
begin
//pre generating the doubles is not required, is just a more useful test for me
l := TList<double>.Create;
for i := 0 to 10000000 do
l.Add(Now/(-i-1)); //some double generation
//
t := TTaskThread.Create(True);
t.Add(l);
t.Start;
end;
end.
FastMM4, par défaut, sur thread contention, lorsqu'un thread ne peut pas acquérir l'accès aux données, verrouillé par un autre thread, appelle la fonction API Windows Sleep(0), et puis, si le verrou n'est pas encore disponible, entre dans une boucle en appelant Sleep(1) après chaque vérification du verrou.
chaque appel au Sommeil (0) subit le coût coûteux d'un commutateur de contexte, qui peut être de 10000+ cycles; il subit également le coût des transitions de l'anneau 3 à l'anneau 0, qui peut être de 1000+ cycles. Comme sur le Sommeil(1) – en plus des coûts associés à Sleep(0) – il retarde également l'exécution d'au moins 1 milliseconde, cédant le contrôle à d'autres threads, et, s'il n'y a pas de threads en attente d'être exécutés par un noyau CPU physique, met le noyau en veille, réduisant ainsi l'utilisation CPU et la consommation d'énergie.
C'est pourquoi, dans votre cas, L'utilisation de CPU n'a jamais atteint 100% - à cause du sommeil(1) émis par FastMM4.
cette façon d'acquérir des serrures n'est pas optimale.
Une meilleure façon serait ont été un spin-lock d'environ 5000 pause
instructions, et, si la serrure était encore occupée, appel à L'appel de L'API SwitchToThread (). Si pause
n'est pas disponible (sur les processeurs très anciens sans support SSE2) ou L'appel API SwitchToThread() n'était pas disponible (sur les versions très anciennes de Windows, antérieures à Windows 2000), la meilleure solution serait d'utiliser EnterCriticalSection/LeaveCriticalSection, qui n'ont pas de latence associée à Sleep (1), et qui cède aussi très efficacement le contrôle du noyau CPU à d'autres threads.
J'ai modifié FastMM4 pour utiliser une nouvelle approche à l'attente d'une serrure: CriticalSections au lieu de Sleep(). Avec ces options, Sleep () ne sera jamais utilisé mais EnterCriticalSection/LeaveCriticalSection sera utilisé à la place. Les tests ont montré que l'approche consistant à utiliser les sections critiques au lieu du sommeil (qui a été utilisé par défaut auparavant dans FastMM4) fournit un gain significatif dans les situations où le nombre de threads travaillant avec le gestionnaire de mémoire est le même ou plus élevé que le nombre de cœurs physiques. Le gain est encore plus évident sur les ordinateurs avec plusieurs CPU physiques et accès à la mémoire non uniforme (NUMA). J'ai mis en place des options de compilation pour supprimer L'approche originale de FastMM4 qui utilise le Sommeil (Temps de sommeil initial) puis le Sommeil (Temps de sommeil supplémentaire) (ou sommeil (0) et le sommeil (1)) et les remplacer par une Sectioncentrecritique/leavec une Sectioncritique pour sauver de précieux cycles CPU perdus par le sommeil(0) et pour améliorer la vitesse (réduire la latence) qui a été affectée à chaque fois par at moins 1 milliseconde par le sommeil(1), parce que les Sections critiques sont beaucoup plus favorables au CPU et ont une latence nettement plus faible que le sommeil (1).
lorsque ces options sont activées, FastMM4-AVX it vérifie:
- si le CPU Supporte SSE2 et donc l'instruction" pause", et
si le système d'exploitation a L'appel API SwitchToThread (), et,
et dans ce cas utilise "pause" boucle de spin pour 5000 itérations et puis SwitchToThread() au lieu des sections critiques; si un CPU n'a pas l'instruction "pause" ou si Windows n'a pas la fonction API SwitchToThread (), il utilisera EnterCriticalSection/LeaveCriticalSection. J'ai mis à disposition la fourche appelée FastMM4-AVX àhttps://github.com/maximmasiutin/FastMM4
Voici la comparaison de la version originale de FastMM4 4.992, avec les options par défaut compilées pour Win64 par Delphi 10.2 Tokyo (version avec Optimisation), et la branche actuelle FastMM4-AVX. Dans certains scénarios, la branche FastMM4-AVX est plus de deux fois plus rapide que la FastMM4 originale. Les tests ont été effectués sur deux ordinateurs différents: un sous Xeon E6-2543v2 avec 2 prises CPU, chacun a 6 noyaux physiques (12 fils logiques) - avec seulement 5 noyaux physiques par prise activée pour l'application de test. Un autre test a été effectué avec un CPU de i7-7700K.
Utilisé le "Multi-threaded allouer, l'utilisation et la libre" et "NexusDB" cas de test de la suite de test FastCode Challenge Memory Manager, modifié pour fonctionner sous 64 bits.
Xeon E6-2543v2 2*CPU i7-7700K CPU
(allocated 20 logical (allocated 8 logical
threads, 10 physical threads, 4 physical
cores, NUMA) cores)
Orig. AVX-br. Ratio Orig. AVX-br. Ratio
------ ----- ------ ----- ----- ------
02-threads realloc 96552 59951 62.09% 65213 49471 75.86%
04-threads realloc 97998 39494 40.30% 64402 47714 74.09%
08-threads realloc 98325 33743 34.32% 64796 58754 90.68%
16-threads realloc 116708 45855 39.29% 71457 60173 84.21%
16-threads realloc 116273 45161 38.84% 70722 60293 85.25%
31-threads realloc 122528 53616 43.76% 70939 62962 88.76%
64-threads realloc 137661 54330 39.47% 73696 64824 87.96%
NexusDB 02 threads 122846 90380 73.72% 79479 66153 83.23%
NexusDB 04 threads 122131 53103 43.77% 69183 43001 62.16%
NexusDB 08 threads 124419 40914 32.88% 64977 33609 51.72%
NexusDB 12 threads 181239 55818 30.80% 83983 44658 53.18%
NexusDB 16 threads 135211 62044 43.61% 59917 32463 54.18%
NexusDB 31 threads 134815 48132 33.46% 54686 31184 57.02%
NexusDB 64 threads 187094 57672 30.25% 63089 41955 66.50%
votre code qui appelle FloatToStr est OK, puisqu'il attribue une chaîne de résultats en utilisant le gestionnaire de mémoire, puis le réaffecte, etc. Même une meilleure idée aurait été explicitement le désallouer, par exemple:
procedure TTaskThread.Execute;
var
i: integer;
s: string;
begin
for i := 0 to 1000000000 do
begin
s := FloatToStr(i*1.31234);
Finalize(s);
end;
end;
vous pouvez trouver de meilleurs tests du gestionnaire de mémoire dans la suite de tests FastCode challenge à http://fastcode.sourceforge.net/