В моей программе я обрабатываю миллионы строк, которые имеют специальный символ, например. "|" для разделения токенов в каждой строке. У меня есть функция, чтобы вернуть маркер n-й, и это он:Есть ли быстрая процедура GetToken для Delphi?
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 и теперь включен в библиотеку Delphi в StrUtils.
Недавно я обновился до Delphi 2009, и мои строки все Unicode. Эта функция GetTok по-прежнему работает и работает хорошо.
Я прошел через новые библиотеки в Delphi 2009 и есть много новых функций и дополнений к нему.
Но я не видел функцию GetToken, как мне нужно, в любой из новых библиотек Delphi, в различных проектах fastcode, и я не могу найти ничего с поиском Google, кроме Zarko Gajic's: Delphi Split/Tokenizer Functions, который не так оптимизирован, как что у меня уже есть.
Любые улучшения, даже 10% будут заметны в моей программе. Я знаю, что альтернатива - это StringLists и всегда держать токены раздельными, но у этого есть большие накладные расходы памяти, и я не уверен, что я сделал все это, чтобы преобразовать, будет ли это быстрее.
Whew. Итак, после всего этого долгого разговора, мой вопрос действительно таков:
Знаете ли вы о каких-либо очень быстрых реализациях процедуры GetToken? Оптимизированная версия ассемблера была бы идеальной?
Если нет, есть ли какие-либо оптимизации, которые вы можете увидеть в моем коде выше, что может улучшить?
Followup: Барри Келли упоминается вопрос, который я задал год назад об оптимизации парсинг строк в файле. В то время я даже не думал о моей программе GetTok, которая не использовалась для чтения или разбора. Только сейчас я увидел накладные расходы на мою процедуру GetTok, которая заставила меня задать этот вопрос. До ответов Карла Смотрича и Барри я никогда не думал о подключении этих двух. Так очевидно, но он просто не регистрировался. Спасибо что подметил это.
Да, мой Делим - это один символ, поэтому, очевидно, у меня есть некоторые основные оптимизации, которые я могу сделать. Мое использование 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/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 }
На бумаге, я не думаю, что вы можете сделать гораздо лучше, чем это.
Поэтому я поставил обе процедуры в задачу и использовал 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);
в то время как линия, которая была выполнена 18 миллионов раз, было сообщено на 2,66 секунды. Было сказано, что линия inc, выполненная 16 миллионов раз, занимает 0,47 секунды.
Теперь я думал, что знаю, что здесь происходит. У меня была аналогичная проблема с AQTime в вопросе, который я задал в прошлом году: Why is CharInSet faster than Case statement?
Опять же, это был Барри Келли, который меня привлек. В принципе, инструмент-профилировщик, такой как 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;
Таким образом, это проверит 1000 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 формата Off.
Суть в том, что новая процедура PChar с исправлением Андреа примерно на 25% быстрее, чем моя оригинальная (0,22 против 0,29).
Я все еще хочу следить за другими комментариями здесь и проверить их.
Отключение оптимизации и включение проверки формата строки увеличивает время от 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/questions/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.
Почему вы перерабатывающие миллионы строк? Возможно, ваша программа может быть оптимизирована, чтобы она не делала этого. –
Этот вопрос является одной из моих попыток оптимизации. Когда у вас есть программа, которая обрабатывает файл размером 300 МБ, она должна будет выполнять много работы, оптимизации и трюки, несмотря ни на что. Но если входной файл подходит к моей большой программе, я не могу сделать его меньше, не обрабатывая его в первую очередь. – lkessler
Спасибо за эту ссылку 1.01pm (интересная дискуссия), но я уверен, что сообщество StackOverflow было бы признательно, если бы материал с другими вещами передавался каким-то другим способом, например. как комментарий к моему блогу. – lkessler