Einzelnen Beitrag anzeigen

choose

Registriert seit: 2. Nov 2003
Ort: Bei Kiel, SH
729 Beiträge
 
Delphi 2006 Architect
 
#1

Stringoperationen :: Performancemessung am bsp ANSI->ASCI

  Alt 5. Dez 2003, 18:19
Hallo X-Dragon,
ich beschreibe im Folgenden Messungen, die sich aus unserer Diskussion im Thread ASCII <-> ANSI ergaben, aber nichts mit der eigentlichen Fragegestellung zu tun haben.

Ausgangspunkt dieser Messung war die Frage nach einer performanteren Funktion, die eine veränderte Kopie eines Eingabestrings zurückgibt, etwa in dieser Form:
Delphi-Quellcode:
function GetModifiedString(const AnInputString: string): string;
var
  iChar: Integer;
begin
  Result:= '';
  for iChar:= 1 to Length(AnInputString) do
  case AnInputString[iChar] of
    ACharOne: Result:= Result+AnotherCharOne;
    ACharTwo: Result:= Result+AnotherCharTwo;
  else
    Result:= Result+AnInputString[iChar];
  end;
end;
Unter der Vorraussetzung, dass die Länge des Ergebnisstrings identisch mit der des Eingabestrings ist, sollte eine Lösung der Form
Delphi-Quellcode:
function GetModifiedString(const AnInputString: string): string;
var
  iChar: Integer;
begin
  Result:= AnInputString;
  for iChar:= Length(AnInputString) downto 1 do
  case Result[iChar] of
    ACharOne: Result[iChar]:= AnotherCharOne;
    ACharTwo: Result[iChar]:= AnotherCharTwo;
  end;
end;
ungleich performanter sein.
Die hier dargestellten Ergebnisse sollen das bestätigen.


Die im Folgenden diskutierte Referenzimplementierung verwendet die von X-Dragon dargestellte Implementierung als "Originalversion" (I) im Vergleich zu einer gemäß der Zielstellung der Messung angepssten Version (II). In Anlehnung an die erwähnte Problemstellung, wird der Durchsatz der Routinen an 131072 (2^17) zufälligen Strings der festen Länge 8 getestet und ausgewertet. Diese Messung wird im Beispiel 8 mal für die jeweilige Implementierung wiederholt, um den Effekt von Einflussgrößen wie Caching, Fragmentierung, etc. zu minimieren bzw. zu erkennen.

Delphi-Quellcode:
program Project1;
{$APPTYPE CONSOLE}
uses
  SysUtils,
  Windows;

type
  TANSIToASCIIFunc = function(const AnInputString: string): string;

//:Orginal implementation (I) by X-Dragon
function AnsiToIBMAscii(const s: string): string;
var
  i : Integer;
begin
  Result:= '';

  for i := 1 to Length(s) do
    case s[i] of
      #196: Result := Result + #142; // von Ä
      #214: Result := Result + #153; // von Ö
      #220: Result := Result + #154; // von Ü
      #228: Result := Result + #132; // von ä
      #246: Result := Result + #148; // von ö
      #252: Result := Result + #129; // von ü
      #223: Result := Result + #225; // von ß
    else
      Result := Result + s[i];
    end;
end;

//:Modified implementation (II)
function AnsiToIBMAsciiRefactored(const AnInput: string): string;
var
  iChar : Integer;
begin
  Result:= AnInput;
  for iChar := Length(Result) downto 1 do
    case Result[iChar] of
      #196: Result[iChar] := #142; // von Ä
      #214: Result[iChar] := #153; // von Ö
      #220: Result[iChar] := #154; // von Ü
      #228: Result[iChar] := #132; // von ä
      #246: Result[iChar] := #148; // von ö
      #252: Result[iChar] := #129; // von ü
      #223: Result[iChar] := #225; // von ß
    end;
end;

//:Tests performance of AFunc against strings of AnArray
procedure DoTest(const AMessage: string; const AnArray: array of string;
  const AFunc: TANSIToASCIIFunc);
var
  iRun: Integer;
  iString: Integer;
  iStartTime: Cardinal;
  iNeedTime: Cardinal;
begin
  // run test eight times
  // might reveal problems with fragmentation
  for iRun:= 0 to 7 do
  begin
    // retrieve start time (in msecs)
    iStartTime:= GetTickCount;
    // call ANSI->ASCII converter for every string in AnArray
    for iString:= Low(AnArray) to High(AnArray) do
      AFunc(AnArray[iString]);
    // calculate needed time (in msecs
    iNeedTime:= GetTickCount-iStartTime;

    // output result
    Writeln(Format('%s (Run %d): %d msec (%d per sec)',
      [AMessage, Succ(iRun), iNeedTime,
       Length(AnArray)*1000 div Succ(iNeedTime)]));
  end;
end;

//:Regression test on given functions against string array
procedure EnsureSameResults(const AStrings: array of string;
  const AFunctions: array of TANSIToASCIIFunc);
var
  iString: Integer;
  iFunc: Integer;
  myResult: string;
begin
  Assert( Length(AFunctions)>=1 );

  // test every string in array
  for iString:= Low(AStrings) to High(AStrings) do
  begin
    // store result of first function in array
    myResult:= AFunctions[0](AStrings[iString]);

    // test stored result against result of every other function in array
    for iFunc:= Succ(Low(AFunctions)) to High(AFunctions) do
      if myResult<>AFunctions[iFunc](AStrings[iString]) then
      begin
        Writeln(Format('ERROR: String(%d)="%s", myResult="%s", AFunctions[%d]="%s"',
          [iString, AStrings[iString], myResult, iFunc,
           AFunctions[iFunc](AStrings[iString])]));
        Readln;
        Halt;
      end;
  end;

  Writeln(Format('Every function (%d) returns same result ',
    [Length(AFunctions)]));
end;

//:Creates random string with length of ALen
function MakeRandomString(const ALen: Cardinal): string;
var
  iChar: Cardinal;
begin
  SetLength(Result, ALen);
  // use only "visible" chars (ASCII-Value>=Space-Value)
  for iChar:=1 to ALen do
    Result[iChar]:= Char(32+Random(224));
end;

var
  myStrings: array[0..131071] of string;
  i: Integer;
begin
  // fill array with random strings
  for i:= Low(myStrings) to High(myStrings) do
    myStrings[i]:= MakeRandomString(8);
  writeln(Format('Created %d random strings', [Length(myStrings)]));

  // test functions
  EnsureSameResults(myStrings, [AnsiToIBMAscii, AnsiToIBMAsciiRefactored]);

  // do test with original version
  DoTest('origin', myStrings, AnsiToIBMAscii);
  // do test with refactored version
  DoTest('refact', myStrings, AnsiToIBMAsciiRefactored);

  Readln;
end.
Die Ausgabe des Programms eines beliebigen Laufs auf einem AMD Athlon 1,2 GHz unter Win2K hatte diese Gestalt:
Code:
Created 131072 random strings
Every function (2) returns same result
origin (Run 1): 771 msec (169782 per sec)
origin (Run 2): 761 msec (172010 per sec)
origin (Run 3): 761 msec (172010 per sec)
origin (Run 4): 762 msec (171785 per sec)
origin (Run 5): 771 msec (169782 per sec)
origin (Run 6): 771 msec (169782 per sec)
origin (Run 7): 771 msec (169782 per sec)
origin (Run 8]: 761 msec (172010 per sec)
refact (Run 1): 80 msec (1618172 per sec)
refact (Run 2): 80 msec (1618172 per sec)
refact (Run 3): 70 msec (1846084 per sec)
refact (Run 4): 80 msec (1618172 per sec)
refact (Run 5): 70 msec (1846084 per sec)
refact (Run 6): 71 msec (1820444 per sec)
refact (Run 7): 90 msec (1440351 per sec)
refact (Run 8]: 70 msec (1846084 per sec)
Es ist zu erkennen, dass beide Varianten mit konstanter Geschwindigkeit arbeiten, also keine Probleme mit Speicherfragementierung oä auftreten (@X-Dragon: Die voreilige Äußerung zur Zunahme der Zeit bei (I) begründete sich in der Tatsache, dass bei jedem Aufruf an Result Konkateniert wurde), und (II) im Vergleich zu (I) 10 mal schneller arbeitet.
Eine Profiler-Analyse (inkl Debug-DCUs mit AQTime3) ergab, für die Unteraufrufe aus (I) und (II) Folgendes:
Code:
Routine Name | % Time w C | Hit Count
--------------+------------+----------      
LStrFromChar |      50,01 |  1015812
LStrCat      |      34,35 |  1048576
LStrClr      |       8,85 |   262144
(body only)  |       4,53 |   131072
LStrLen      |       2,26 |   131072

                    Ergebnisse für (I)



Routine Name | % Time w C | Hit Count
--------------+------------+---------
(body only)  |      43,18 |   131072
LStrAsg      |      41,22 |   131072
UniqueStringA |      15,33 |    32764
LStrLen      |       0,27 |   131072

                   Ergebnisse für (II)
Die Tabellen zeigen in der Spalte "% Time with Children" die "verbrachte Zeit" in Unteraufrufen bzw. dem Code der Routine selbst ("body only") sowie die Anzahl der Aufrufe bei 131072 zu verarbeitenen Strings.

Es ist deutlich zu erkennen, dass (I) den Großteil der Zeit (>95%) mit der Verarbeitung von Strings (Alloziierung, Kopieren, etc.) statt mit der Fallunterscheidung, der eigentlichen Programmlogik, zubringt. In (II) hingegen, wird über 40% der Zeit für die Programmlogik genutzt und auch die Funktionen zur Stringverarbeitung werden maximal einmal pro Aufruf von (II) verwendet. Die Lösung (I) hingegen verwendet selbige etwa achtmal häufiger, was der Länge der zu verarbeitenen Strings entspricht.

Die Testergebnisse zeigen, dass die Variante (II) bedingt durch effizientere Stringverarbeitung im Rahmen des Testszenarios etwa 10 mal schneller ist.
Strings der Länge 4096 konnten diesen Quotienten auf über 26 Erhöhen.

Ich hoffe, dass die Ergebnisse dieses Beitrags helfen können, Programme mit Stringverarbeitung performanter zu gestalten, ohne direkten Speicherzugriff, zB in Form von PChar oder ASM, verwenden zu müssen.

[edit=sakura] Leerzeichen eingefügt Mfg, sakura[/edit]
gruß, choose
  Mit Zitat antworten Zitat