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.

10
demandé sur Alex 2015-01-22 03:22:57

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 sprintfmsvcrt.dll. Cela fournira une preuve de concept.

4
répondu David Heffernan 2015-01-22 07:12:36

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.

4
répondu kludg 2015-01-22 07:25:01

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.

0
répondu Valeriy 2015-01-22 08:02:25

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.
0
répondu Alex 2015-01-23 02:44:37

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/

0
répondu Maxim Masiutin 2017-07-14 05:38:09