Быстрое заполнение строки в Delphi

Я пытался ускорить определенную процедуру в приложении, и мой профилировщик AQTime определил один метод, в частности, как узкое место. Метод был с нами в течение многих лет и является частью "разное"-единицы:

function cwLeftPad(aString:string; aCharCount:integer; aChar:char): string;
var
  i,vLength:integer;
begin
  Result := aString;
  vLength := Length(aString);
  for I := (vLength + 1) to aCharCount do    
    Result := aChar + Result;
end;

в той части программы, которую я оптимизирую на данный момент, метод был вызван ~35k раз, и это заняло потрясающее 56% времени выполнения!

легко видеть, что это ужасный способ левой прокладки строки, поэтому я заменил его с

function cwLeftPad(const aString:string; aCharCount:integer; aChar:char): string; 
begin
  Result := StringOfChar(aChar, aCharCount-length(aString))+aString;
end;

что дало значительный импульс. Общее время работы прошло от 10,2 сек до 5,4 сек. Потрясающе! Но cwLeftPad по-прежнему составляет около 13% от общего времени работы. Есть ли простой способ оптимизировать этот метод дальше?

7 ответов


Ваша новая функция включает в себя три строки, вход, результат от StringOfChar, и результат функции. Один из них уничтожается, когда функция возвращает. Вы можете сделать это за два, ничего не уничтожая и не перераспределяя.

  1. выделите строку общей требуемой длины.
  2. заполните первую часть его своим символом заполнения.
  3. заполните оставшуюся часть входной строкой.

вот пример:

function cwLeftPad(const aString: AnsiString; aCharCount: Integer; aChar: AnsiChar): AnsiString;
var
  PadCount: Integer;
begin
  PadCount := ACharCount - Length(AString);
  if PadCount > 0 then begin
    SetLength(Result, ACharCount);
    FillChar(Result[1], PadCount, AChar);
    Move(AString[1], Result[PadCount + 1], Length(AString));
  end else
    Result := AString;
end;

Я не знаю, предоставляют ли Delphi 2009 и более поздние версии двухбайтовый эквивалент FillChar на основе Char, и если они это делают, я не знаю, как это называется, поэтому я изменил сигнатуру функции, чтобы явно использовать AnsiString. Если вам нужен WideString или UnicodeString, вам нужно найти замену FillChar, которая обрабатывает двухбайтовые символы. (FillChar имеет запутанное имя с Delphi 2009, так как он не обрабатывает полноразмерный символ ценности.)

еще одна вещь, чтобы рассмотреть, действительно ли вам нужно так часто вызывать эту функцию в первую очередь. Самый быстрый код-это код, который не работает.


другая мысль - если это Delphi 2009 или 2010, отключите "проверку строкового формата" в Project, Options, Delphi Compiler, Compiling, Code Generation.


StringOfChar очень быстро и я сомневаюсь, что вы можете улучшить этот код много. Тем не менее, попробуйте этот, может быть, это быстрее:

function cwLeftPad(aString:string; aCharCount:integer; aChar:char): string;
var
  i,vLength:integer;
  origSize: integer;
begin
  Result := aString;
  origSize := Length(Result);
  if aCharCount <= origSize then
    Exit;
  SetLength(Result, aCharCount);
  Move(Result[1], Result[aCharCount-origSize+1], origSize * SizeOf(char));
  for i := 1 to aCharCount - origSize do
    Result[i] := aChar;
end;

EDIT: я провел некоторое тестирование, и моя функция медленнее, чем ваша улучшенная cwLeftPad. Но я нашел кое - что еще-вашему процессору не нужно 5 секунд для выполнения функций 35k cwLeftPad, за исключением случаев, когда вы работаете на PC XT или форматируете строки gigabyte.

Я тестировал с помощью этого простого кода

for i := 1 to 35000 do begin
  a := 'abcd1234';
  b := cwLeftPad(a, 73, '.');
end;

и я получил 255 миллисекунды для исходного cwLeftPad, 8 миллисекунд для улучшенного cwLeftPad и 16 миллисекунд для моей версии.


теперь вы вызываете StringOfChar каждый раз. Конечно, этот метод проверяет, есть ли у него что-то делать, и выпрыгивает, если длина достаточно мала, но, возможно, вызов StringOfChar занимает много времени, потому что внутри он делает другой вызов перед выпрыгиванием.

поэтому моей первой идеей было бы выскочить самостоятельно, если нечего делать:

function cwLeftPad(const aString: string; aCharCount: Integer; aChar: Char;): string;
var
  l_restLength: Integer;
begin
  Result  := aString;
  l_restLength := aCharCount - Length(aString);
  if (l_restLength < 1) then
    exit;

  Result := StringOfChar(aChar, l_restLength) + aString;
end;

вы можете ускорить эту процедуру еще больше, используя массив подстановок.

конечно, это зависит от ваших требований. Если вы не против потратить немного памяти... Я предполагаю, что функция называется 35 k раз, но она не имеет 35000 разных длин заполнения и много разных символов.

поэтому, если вы знаете (или можете быстро оценить) диапазон прокладок и символов заполнения, вы можете построить двумерный массив, который включает эти параметры. Для для простоты я предполагаю, что у вас есть 10 разных длин заполнения, и вы заполняете одним символом ... - так, например, это будет одномерный массив.

вы реализуете это следующим образом:

type
  TPaddingArray = array of String;

var
  PaddingArray: TPaddingArray;
  TestString: String;

function cwLeftPad4(const aString:string; const aCharCount:integer; const aChar:char; var anArray: TPaddingArray ): string;
begin
  Result := anArray[aCharCount-length(aString)] + aString;
end;

begin
  //fill up the array
  SetLength(StrArray, 10);
  PaddingArray[0] := '';
  PaddingArray[1] := '.';
  PaddingArray[2] := '..';
  PaddingArray[3] := '...';
  PaddingArray[4] := '....';
  PaddingArray[5] := '.....';
  PaddingArray[6] := '......';
  PaddingArray[7] := '.......';
  PaddingArray[8] := '........';
  PaddingArray[9] := '.........';

  //and you call it..
  TestString := cwLeftPad4('Some string', 20, '.', PaddingArray);
end;

вот результаты:

Time1 - oryginal cwLeftPad          : 27,0043604142394 ms.
Time2 - your modyfication cwLeftPad : 9,25971967336897 ms.
Time3 - Rob Kennedy's version       : 7,64538131122457 ms.
Time4 - cwLeftPad4                  : 6,6417059620664 ms.

обновили ориентиры:

Time1 - oryginal cwLeftPad          : 26,8360194218451 ms.
Time2 - your modyfication cwLeftPad : 9,69653117046119 ms.
Time3 - Rob Kennedy's version       : 7,71149259179622 ms.
Time4 - cwLeftPad4                  : 6,58248533610693 ms.
Time5 - JosephStyons's version      : 8,76641780969192 ms.

вопрос в том, стоит ли это хлопот?;-)


возможно, будет быстрее использовать StringOfChar для выделения совершенно новой строки длины строки и заполнения, а затем использовать move для копирования существующего текста поверх нее.
Я думаю, что вы создаете две новые строки выше (одна с FillChar и одна с плюсом). Для этого требуется выделить две памяти и конструкции строкового псевдо-объекта. Это будет медленно. Может быть быстрее потратить несколько циклов процессора, делая некоторые избыточные заполнения, чтобы избежать дополнительных операции с памятью.
Это может быть еще быстрее, если вы выделили пространство памяти, а затем сделали FillChar и Move, но дополнительный вызов fn может замедлить это.
Такие вещи часто бывают методом проб и ошибок!


вы можете получить значительно лучшую производительность, если вы предварительно выделить строку.

function cwLeftPadMine
{$IFDEF VER210}  //delphi 2010
(aString: ansistring; aCharCount: integer; aChar: ansichar): ansistring;
{$ELSE}
(aString: string; aCharCount: integer; aChar: char): string;
{$ENDIF}
var
  i,n,padCount: integer;
begin
  padCount := aCharCount - Length(aString);

  if padCount > 0 then begin
    //go ahead and set Result to what it's final length will be
    SetLength(Result,aCharCount);
    //pre-fill with our pad character
    FillChar(Result[1],aCharCount,aChar);

    //begin after the padding should stop, and restore the original to the end
    n := 1;
    for i := padCount+1 to aCharCount do begin
      Result[i] := aString[n];
    end;
  end
  else begin
    Result := aString;
  end;
end;

и вот шаблон, который полезен для сравнения:

procedure TForm1.btnPadTestClick(Sender: TObject);
const
  c_EvalCount = 5000;  //how many times will we run the test?
  c_PadHowMany = 1000;  //how many characters will we pad
  c_PadChar = 'x';  //what is our pad character?
var
  startTime, endTime, freq: Int64;
  i: integer;
  secondsTaken: double;
  padIt: string;
begin
  //store the input locally
  padIt := edtPadInput.Text;

  //display the results on the screen for reference
  //(but we aren't testing performance, yet)
  edtPadOutput.Text := cwLeftPad(padIt,c_PadHowMany,c_PadChar);

  //get the frequency interval of the OS timer    
  QueryPerformanceFrequency(freq);

  //get the time before our test begins
  QueryPerformanceCounter(startTime);

  //repeat the test as many times as we like
  for i := 0 to c_EvalCount - 1 do begin
    cwLeftPad(padIt,c_PadHowMany,c_PadChar);
  end;

  //get the time after the tests are done
  QueryPerformanceCounter(endTime);

  //translate internal time to # of seconds and display evals / second
  secondsTaken := (endTime - startTime) / freq;
  if secondsTaken > 0 then begin
    ShowMessage('Eval/sec = ' + FormatFloat('#,###,###,###,##0',
      (c_EvalCount/secondsTaken)));
  end
  else begin
    ShowMessage('No time has passed');
  end;
end;

используя этот шаблон бенчмарка, я получаю следующие результаты:

The original: 5,000 / second
Your first revision: 2.4 million / second
My version: 3.9 million / second
Rob Kennedy's version: 3.9 million / second