Подтвердить что ты не робот

Есть ли быстрая процедура GetToken для Delphi?

В моей программе я обрабатываю миллионы строк, которые имеют специальный символ, например. "|" для разделения токенов в каждой строке. У меня есть функция, чтобы вернуть n-й токен, и это он:

function GetTok(const Line: string; const Delim: string; const TokenNum: Byte): string;
{ LK Feb 12, 2007 - This function has been optimized as best as possible }
var
 I, P, P2: integer;
begin
  P2 := Pos(Delim, Line);
  if TokenNum = 1 then begin
    if P2 = 0 then
      Result := Line
    else
      Result := copy(Line, 1, P2-1);
  end
  else begin
    P := 0; { To prevent warnings }
    for I := 2 to TokenNum do begin
      P := P2;
      if P = 0 then break;
      P2 := PosEx(Delim, Line, P+1);
    end;
    if P = 0 then
      Result := ''
    else if P2 = 0 then
      Result := copy(Line, P+1, MaxInt)
    else
      Result := copy(Line, P+1, P2-P-1);
  end;
end; { GetTok }

Я разработал эту функцию, когда я использовал Delphi 4. Он вызывает очень эффективную процедуру PosEx, которая была первоначально разработана Fastcode и теперь включена в библиотеку StrUtils Delphi.

Недавно я обновился до Delphi 2009, и мои строки все Unicode. Эта функция GetTok по-прежнему работает и работает хорошо.

Я прошел через новые библиотеки в Delphi 2009, и есть много новых функций и дополнений к нему.

Но я не видел функцию GetToken, как мне нужно, в любой из новых библиотек Delphi, в различных проектах fastcode, и я не могу найти ничего с поиском Google, кроме Zarko Gajic's: Delphi Split/Tokenizer Functions, который не так оптимизирован, как у меня уже есть.

Любое улучшение, даже 10% было бы заметно в моей программе. Я знаю, что альтернатива - это StringLists и всегда держать токены раздельными, но у этого есть большие накладные расходы памяти, и я не уверен, что я сделал все это, чтобы преобразовать, будет ли это быстрее.

Уф. Итак, после всего этого долгого разговора, мой вопрос на самом деле:

Знаете ли вы о каких-либо очень быстрых реализациях процедуры GetToken? Оптимизированная версия ассемблера была бы идеальной?

Если нет, есть ли какие-либо оптимизации, которые вы можете увидеть в моем коде выше, что может улучшить?


Followup: Барри Келли упомянул вопрос, который я задал год назад об оптимизации синтаксического анализа строк в файле. В то время я даже не думал о моей программе GetTok, которая не использовалась для чтения или разбора. Только сейчас я увидел накладные расходы на мою процедуру GetTok, которая заставила меня задать этот вопрос. Пока Карл Смотрич и Барри не ответили, я никогда не думал о подключении этих двух. Так очевидно, но он просто не регистрировался. Спасибо, что указали это.

Да, мой Delim - это один символ, поэтому, очевидно, у меня есть какая-то крупная оптимизация, которую я могу сделать. Мое использование Pos и ​​PosEx в методе GetTok (выше) ослепило меня мыслью, что я могу сделать это быстрее с символом путем поиска символа вместо этого, с битами кода вроде:

      while (cp^ > #0) and (cp^ <= Delim) do    
        Inc(cp);

Я собираюсь пройти все ответы и попробовать различные предложения и сравнить их. Затем я опубликую результаты.


Путаница: Ладно, теперь я действительно озадачен.

Я принял рекомендации Карла и Барри, чтобы пойти с PChars, и вот моя реализация:

function GetTok(const Line: string; const Delim: string; const TokenNum: Byte): string;
{ LK Feb 12, 2007 - This function has been optimized as best as possible }
{ LK Nov 7, 2009 - Reoptimized using PChars instead of calls to Pos and PosEx }
{ See; https://stackoverflow.com/info/1694001/is-there-a-fast-gettoken-routine-for-delphi }
var
 I: integer;
 PLine, PStart: PChar;
begin
  PLine := PChar(Line);
  PStart := PLine;
  inc(PLine);
  for I := 1 to TokenNum do begin
    while (PLine^ <> #0) and (PLine^ <> Delim) do
      inc(PLine);
    if I = TokenNum then begin
      SetString(Result, PStart, PLine - PStart);
      break;
    end;
    if PLine^ = #0 then begin
      Result := '';
      break;
    end;
    inc(PLine);
    PStart := PLine;
  end;
end; { GetTok }

На бумаге я не думаю, что вы можете сделать намного лучше этого.

Итак, я поставил обе процедуры в задачу и использовал AQTime, чтобы узнать, что происходит. Запуск, который я включил 1 108 514 звонков в GetTok.

AQTime запустил исходную процедуру на 0,40 секунды. Миллион звонков в Pos занял 0,10 секунды. Полумиллион TokenNum = 1 копия заняла 0,10 секунды. 600 000 звонков PosEx заняли всего 0,03 секунды.

Затем я приурочил свою новую процедуру с AQTime для одного и того же прогона и точно так же звонил. AQTime сообщает, что моя новая "быстрая" процедура заняла 3,65 секунды, что в 9 раз больше. Преступником по AQTime был первый цикл:

     while (PLine^ <> #0) and (PLine^ <> Delim) do
       inc(PLine);

Линия while, выполненная 18 миллионов раз, была зарегистрирована в 2,66 секунды. Было сказано, что линия inc, выполненная 16 миллионов раз, занимает 0,47 секунды.

Теперь я подумал, что знаю, что здесь происходит. У меня была аналогичная проблема с AQTime в вопросе, который я задал в прошлом году: Почему CharInSet быстрее, чем оператор Case?

Снова это был Барри Келли, который меня привлек. В принципе, инструмент-профилировщик, такой как AQTime, не обязательно выполняет работу по микрооптимизации. Он добавляет накладные расходы каждой строке, которая может затушить результаты, которые четко показаны в этих числах. 34 миллиона строк, выполненных в моем новом "оптимизированном коде", перегружают несколько миллионов строк моего исходного кода, по-видимому, мало или вообще не накладные расходы из подпрограмм Pos и ​​PosEx.

Барри дал мне образец кода с помощью QueryPerformanceCounter, чтобы проверить, что он был прав, и в этом случае он был.

Хорошо, давайте теперь сделаем то же самое теперь с QueryPerformanceCounter, чтобы доказать, что моя новая процедура работает быстрее, а не в 9 раз медленнее, как говорит AQTime. Итак, я иду:

function TimeIt(const Title: string): double;
var  i: Integer;
  start, finish, freq: Int64;
  Seconds: double;
begin
  QueryPerformanceCounter(start);
  for i := 1 to 250000 do
    GetTokOld('This is a string|that needs|parsing', '|', 1);
  for i := 1 to 250000 do
    GetTokOld('This is a string|that needs|parsing', '|', 2);
  for i := 1 to 250000 do
    GetTokOld('This is a string|that needs|parsing', '|', 3);
  for i := 1 to 250000 do
    GetTokOld('This is a string|that needs|parsing', '|', 4);
  QueryPerformanceCounter(finish);
  QueryPerformanceFrequency(freq);
  Seconds := (finish - start) / freq;
  Result := Seconds;
end;

Итак, это проверит 1,000,000 звонков в GetTok.

Моя старая процедура с вызовами Pos и ​​PosEx заняла 0.29 секунды. Новый с PChars занял 2.07 секунды.

Теперь я полностью одурачен! Может ли кто-нибудь сказать мне, почему процедура PChar не только медленнее, но и в 8-9 раз медленнее!?


Тайна решена! Андреас сказал в своем ответе на изменение параметра Delim от строки до Char. Я всегда буду использовать только Char, поэтому, по крайней мере, для моей реализации это очень возможно. Я был поражен тем, что произошло.

Время для 1 миллиона вызовов снизилось с 1,88 секунды до 0,22 секунды.

И удивительно, что время для моей исходной процедуры Pos/PosEx увеличилось с 0,29 до 0,44 секунды, когда я изменил параметр Delim на Char.

Честно говоря, я разочарован оптимизатором Delphi. Этот разделитель является постоянным параметром. Оптимизатор должен был заметить, что одно и то же преобразование происходит в цикле и должно было переместить его так, чтобы он выполнялся только один раз.

Двойная проверка параметров генерации кода, да, у меня есть проверка оптимизации True и String.

Нижняя линия заключается в том, что новая процедура PChar с исправлением Andrea примерно на 25% быстрее, чем моя оригинальная (.22 против .29).

Я все еще хочу следить за другими комментариями здесь и тестировать их.


Отключение оптимизации и включение проверки формата String увеличивает время от 0,22 до 0,30. Он добавляет примерно то же самое к оригиналу.

Преимущество использования кода ассемблера или вызовов, написанных на ассемблере, таком как Pos или PosEx, заключается в том, что они НЕ подпадают под какие параметры генерации кода, которые вы установили. Они всегда будут работать одинаково, предварительно оптимизирован и не раздуты.

Я за последние пару дней подтвердил, что лучший способ сравнить код для микрооптимизации - это посмотреть и сравнить код Ассемблера в окне CPU. Было бы неплохо, если бы Embarcadero мог сделать это окно немного более удобным и позволить нам копировать фрагменты в буфер обмена или печатать его разделы.

Кроме того, я несправедливо захлопнул AQTime раньше в этом сообщении, думая, что дополнительное время, добавленное для моей новой процедуры, было исключительно из-за добавленной инструментализации. Теперь, когда я возвращаюсь и проверяю параметр Char вместо String, цикл while сокращается до 0,30 секунды (от 2,66), а строка inc - до 0,14 секунды (от .47). Странно, что линия inc также снизится. Но я уже устал от всего этого тестирования.


Я взял идею Карла о петлях персонажей и переписал этот код с этой идеей. Это делает еще одно улучшение, вплоть до 0,19 секунды с 0,22. Итак, теперь это самое лучшее:

function GetTok(const Line: string; const Delim: Char; const TokenNum: Byte): string;
{ LK Nov 8, 2009 - Reoptimized using PChars instead of calls to Pos and PosEx }
{ See; https://stackoverflow.com/info/1694001/is-there-a-fast-gettoken-routine-for-delphi }
var
  I, CurToken: Integer;
  PLine, PStart: PChar;
begin
  CurToken := 1;
  PLine := PChar(Line);
  PStart := PLine;
  for I := 1 to length(Line) do begin
    if PLine^ = Delim then begin
      if CurToken = TokenNum then
        break
      else begin
        CurToken := CurToken + 1;
        inc(PLine);
        PStart := PLine;
      end;
    end
    else
      inc(PLine);
  end;
  if CurToken = TokenNum then
    SetString(Result, PStart, PLine - PStart)
  else
    Result := '';
end;

По-прежнему могут быть некоторые незначительные оптимизации, такие как сравнение CurToken = Tokennum, которое должно быть того же типа: Integer или Byte, в зависимости от того, что быстрее.

Но позвольте сказать, теперь я счастлив.

Еще раз спасибо сообществу StackOverflow Delphi.

4b9b3361

Ответ 1

Ваша новая функция (одна с PChar) должна объявить "Delim" как Char, а не как Строка. В вашей текущей реализации компилятор должен преобразовать PLine ^ char в строку, чтобы сравнить ее с "Delim". И это происходит в плотной петле, что приводит к огромному результату.

function GetTok(const Line: string; const Delim: Char{<<==}; const TokenNum: Byte): string;
{ LK Feb 12, 2007 - This function has been optimized as best as possible }
{ LK Nov 7, 2009 - Reoptimized using PChars instead of calls to Pos and PosEx }
{ See; http://stackoverflow.com/questions/1694001/is-there-a-fast-gettoken-routine-for-delphi }
var
 I: integer;
 PLine, PStart: PChar;
begin
  PLine := PChar(Line);
  PStart := PLine;
  inc(PLine);
  for I := 1 to TokenNum do begin
    while (PLine^ <> #0) and (PLine^ <> Delim) do
      inc(PLine);
    if I = TokenNum then begin
      SetString(Result, PStart, PLine - PStart);
      break;
    end;
    if PLine^ = #0 then begin
      Result := '';
      break;
    end;
    inc(PLine);
    PStart := PLine;
  end;
end; { GetTok }

Ответ 2

Это имеет большое значение, как ожидается, "Delim". Если бы он ожидал быть единственным символом, вам намного лучше было бы переходить по строковому символу по символу, в идеале через PChar и конкретно тестировать.

Если это длинная строка, Boyer-Moore и подобные поиски имеют фазу настройки для пропущенных таблиц, и лучший способ - создать таблицы один раз и повторно использовать их для каждой последующей находки. Это означает, что вам нужно состояние между вызовами, и эта функция будет лучше использоваться как метод для объекта.

Вам может быть интересен этот ответ, который я задал на вопрос раньше, о самом быстром анализе строки в Delphi. (Но я вижу, что это это вопрос, который задал вопрос! Тем не менее, при решении вашей проблемы я хотел бы понять, как я описал синтаксический анализ не, используя PosEx, как вы используете, в зависимости от того, как обычно выглядит Delim.)


ОБНОВЛЕНИЕ: Хорошо, я потратил около 40 минут на это. Если вы знаете, что разделитель будет символом, вам будет намного лучше со второй версией (например, сканирование PChar), но вы должны передать Delim в качестве символа. Во время написания вы преобразовываете выражение PLine^ - типа Char - в строку для сравнения с Delim. Это будет очень медленно; даже индексирование в строку, с Delim[1] также будет несколько медленным.

Однако, в зависимости от того, насколько велики ваши строки и сколько разделенных фрагментов вы хотите вытащить, вам может быть лучше с возобновляемым подходом, а не пропуски нежелательных разделенных фрагментов внутри подпрограммы токенизации. Если вы вызываете GetTok с последовательно увеличивающимися индексами, например, вы делаете сейчас в своем мини-тесте, вы получите производительность O (n * n), где n - количество разделенных разделов. Это можно преобразовать в O (n), если вы сохраните состояние сканирования и восстановите его для следующей итерации или упакуйте все извлеченные элементы в массив.

Здесь версия, которая делает все токенизацию один раз и возвращает массив. Для того, чтобы знать, насколько велика возможность создания массива, нужно дважды выполнить токенизацию. С другой стороны, только вторая токенизация должна извлекать строки:

// Do all tokenization up front.
function GetTok4(const Line: string; const Delim: Char): TArray<string>;
var
  cp, start: PChar;
  count: Integer;
begin
  // Count sections
  count := 1;
  cp := PChar(Line);
  start := cp;
  while True do
  begin
    if cp^ <> #0 then
    begin
      if cp^ <> Delim then
        Inc(cp)
      else
      begin
        Inc(cp);
        Inc(count);
      end;
    end
    else
    begin
      Inc(count);
      Break;
    end;
  end;

  SetLength(Result, count);
  cp := start;
  count := 0;

  while True do
  begin
    if cp^ <> #0 then
    begin
      if cp^ <> Delim then
        Inc(cp)
      else
      begin
        SetString(Result[count], start, cp - start);
        Inc(cp);
        Inc(count);
      end;
    end
    else
    begin
      SetString(Result[count], start, cp - start);
      Break;
    end;
  end;
end;

Здесь возможен возобновляемый подход. Однако нагрузки и запасы текущего положения и разделителя имеют стоимость, однако:

type
  TTokenizer = record
  private
    FSource: string;
    FCurrPos: PChar;
    FDelim: Char;
  public
    procedure Reset(const ASource: string; ADelim: Char); inline;
    function GetToken(out AResult: string): Boolean; inline;
  end;

procedure TTokenizer.Reset(const ASource: string; ADelim: Char);
begin
  FSource := ASource; // keep reference alive
  FCurrPos := PChar(FSource);
  FDelim := ADelim;
end;

function TTokenizer.GetToken(out AResult: string): Boolean;
var
  cp, start: PChar;
  delim: Char;
begin
  // copy members to locals for better optimization
  cp := FCurrPos;
  delim := FDelim;

  if cp^ = #0 then
  begin
    AResult := '';
    Exit(False);
  end;

  start := cp;
  while (cp^ <> #0) and (cp^ <> Delim) do
    Inc(cp);

  SetString(AResult, start, cp - start);
  if cp^ = Delim then
    Inc(cp);
  FCurrPos := cp;
  Result := True;
end;

Здесь полная программа, которую я использовал для бенчмаркинга.

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

*** count=3, Length(src)=200
GetTok1: 595 ms
GetTok2: 547 ms
GetTok3: 2366 ms
GetTok4: 407 ms
GetTokBK: 226 ms
*** count=6, Length(src)=350
GetTok1: 1587 ms
GetTok2: 1502 ms
GetTok3: 6890 ms
GetTok4: 679 ms
GetTokBK: 334 ms
*** count=9, Length(src)=500
GetTok1: 3055 ms
GetTok2: 2912 ms
GetTok3: 13766 ms
GetTok4: 947 ms
GetTokBK: 446 ms
*** count=12, Length(src)=650
GetTok1: 4997 ms
GetTok2: 4803 ms
GetTok3: 23021 ms
GetTok4: 1213 ms
GetTokBK: 543 ms
*** count=15, Length(src)=800
GetTok1: 7417 ms
GetTok2: 7173 ms
GetTok3: 34644 ms
GetTok4: 1480 ms
GetTokBK: 653 ms

В зависимости от характеристик ваших данных, может ли разделитель быть персонажем или нет, и как вы с ним работаете, разные подходы могут быть более быстрыми.

(Я ошибся в своей предыдущей программе, я не измерял одни и те же операции для каждого стиля подпрограммы. Я обновил ссылку на pastebin и результаты теста.)

Ответ 3

Delphi компилирует ОЧЕНЬ эффективный код; по моему опыту, было очень сложно сделать лучше в ассемблере.

Я думаю, вы должны просто указать PChar (они все еще существуют, не так ли? Я расстался с Delphi около 4.0) в начале строки и увеличил ее при подсчете "|", пока вы не нашли n-1 из них. Я подозреваю, что это будет быстрее, чем вызов PosEx повторно.

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

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

EDIT: Вот что я имел в виду. Этот код, увы, не скомпилирован и не проверен, но должен продемонстрировать, что я имел в виду.

В частности, Delim рассматривается как единственный char, который, как я считаю, создает мир различий, если он будет соответствовать требованиям, а персонаж в PLine проверяется только один раз. Наконец, больше нет сравнения с TokenNum; Я считаю, что быстрее для уменьшения счетчика 0 для подсчета разделителей.

function GetTok(const Line: string; const Delim: string; const TokenNum: Byte): string;
var 
  Del: Char;
  PLine, PStart: PChar;
  Nth, I, P0, P9: Integer;
begin
  Del := Delim[1];
  Nth := TokenNum + 1;
  P0 := 1;
  P9 := Line.length + 1;
  PLine := PChar(line);
  for I := 1 to P9 do begin
    if PLine^ = Del then begin
      if Nth = 0 then begin
        P9 := I;
        break;
      end;
      Dec(Nth);
      if Nth = 0 then P0 := I + 1
    end;
    Inc(PLine);
  end;
  if (Nth <= 1) or (TokenNum = 1) then
    Result := Copy(Line, P0, P9 - P0);
  else
    Result := '' 
end;

Ответ 4

Использование ассемблера - это микро-оптимизация. Оптимизация алгоритма достигается гораздо большим успехом. Не делайте работу, делая работу как можно быстрее, каждый раз.

Например, если у вас есть места в вашей программе, где вам нужно несколько токенов одной строки. Другая процедура, которая возвращает массив токенов, которые вы можете индексировать, должна быть быстрее, чем вызов вашей функции более одного раза, особенно если вы позволите процедуре не возвращать все токены, но только столько, сколько вам нужно.

Но в целом я согласен с ответом Carl (+1), используя PChar для сканирования, вероятно, будет быстрее, чем ваш текущий код.

Ответ 5

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

На самом деле у меня есть несколько других подпрограмм, CountSections и ParseSectionPOS - это несколько примеров.

К сожалению, эта подпрограмма основана только на ansi/pchar. Хотя я не думаю, что было бы трудно переместить его в unicode. Возможно, я уже сделал это... Я должен это проверить.

Примечание. Эта процедура 1 основана на индексировании ParseNum.

function ParseSection(ParseLine: string; ParseNum: Integer; ParseSep: Char; QuotedStrChar:char = #0) : string;
var
   wStart, wEnd : integer;
   wIndex : integer;
   wLen : integer;
   wQuotedString : boolean;
begin
   result := '';
   wQuotedString := false;
   if not (ParseLine = '') then
   begin
      wIndex := 1;
      wStart := 1;
      wEnd := 1;
      wLen := Length(ParseLine);
      while wEnd <= wLen do
      begin
         if (QuotedStrChar <> #0) and (ParseLine[wEnd] = QuotedStrChar) then
            wQuotedString := not wQuotedString;

         if not wQuotedString and (ParseLine[wEnd] = ParseSep) then
         begin
            if wIndex=ParseNum then
               break
            else
            begin
               inc(wIndex);
               wStart := wEnd+1;
            end;
         end;
         inc(wEnd);
      end;

      result := copy(ParseLine, wStart, wEnd-wStart);
      if (length(result) > 0) and (QuotedStrChar <> #0) and (result[1] = QuotedStrChar) then
         result := AnsiDequotedStr(result, QuotedStrChar);
   end;
end; { ParseSection }

Ответ 6

В вашем коде я думаю, что это единственная строка, которую можно оптимизировать:

Result := copy(Line, P+1, MaxInt)

Если вы вычисляете новую длину там, она может быть немного быстрее, но не 10%, которую вы ищете.

Ваш алгоритм токенизации выглядит довольно хорошо. Для его оптимизации я запускал его через профилировщик (например, AQTime из AutomatedQA) с представительным подмножеством ваших производственных данных. Это укажет вам на самое слабое место.

Единственная функция RTL, которая приближается, - это единица в классе:

procedure TStrings.SetDelimitedText(const Value: string);

Он токенизирует, но использует как QuoteChar, так и Разделитель, но вы используете только разделитель.

Он использует функцию SetString в системном блоке, что является довольно быстрым способом установки содержимого строки на основе PChar/PAnsiChar/PUnicodeChar и длина.

Это может привести к некоторому улучшению; с другой стороны, Копировать тоже очень быстро.

Ответ 7

Я не тот, кто всегда обвиняет алгоритм, но если я посмотрю на первую часть источника, проблема в том, что для строки N вы снова используете POS/posexes для строки 1..n-1.

Это означает, что для N элементов вы суммируете (n, n-1, n-2... 1) POSes (= +/- 0.5 * N ^ 2), тогда как требуется только N.

Если вы просто кэшируете позицию последнего найденного результата, например. в записи, которая передается параметром VAR, вы можете получить много.

тип
   TLastPosition = запись                      elementnr: integer;//последний tokennumber                      elementpos: integer;//символьный индекс последнего совпадения                      конец;

а затем что-то

если tokennum = (lastposition.elementnr + 1), тогда   начать     newpos: = posex (DELIM, линии, lastposition.elementpos);   конец;

К сожалению, у меня нет времени писать, но я надеюсь, что вы поймете идею