Почему CharInSet быстрее, чем оператор Case?

Я в недоумении. Сегодня на CodeRage Марко Канту сказал, что CharInSet работает медленно, и вместо этого я должен попробовать оператор Case. Я сделал это в своем парсере, а затем проверил с помощью AQTime, каково было ускорение. Я обнаружил, что оператор Case работает намного медленнее.

4 894 539 казней:

пока не CharInSet (P ^, ['', # 10, # 13, # 0]) do inc (P);

был рассчитан на 0,25 секунды.

Но столько же казней:

while True do
case P ^ of
'', # 10, # 13, # 0: break;
else inc (P);
end;

занимает 0,16 секунды для «while True», 0,80 секунды для первого случая и 0,13 секунды для другого случая, в сумме 1,09 секунды или более чем в 4 раза дольше.

Код ассемблера для оператора CharInSet:

add edi, $ 02
mov edx, $ 0064b290
movzx eax, [edi]
вызов CharInSet
тест a1, a1
jz $ 00649f18 (назад к оператору добавления)

тогда как логика случая проста:

movzx eax, [edi]
sub ax, $ 01
jb $ 00649ef0
sub ax, $ 09
jz $ 00649ef0
sub ax, $ 03
jz $ 00649ef0
добавить edi , $ 02
jmp $ 00649ed6 (назад к инструкции movzx)

Мне кажется, что логика case использует очень эффективный ассемблер, тогда как оператор CharInSet фактически должен вызывать функцию CharInSet, которая находится в SysUtils и также проста, а именно:

function CharInSet (C: AnsiChar; const CharSet: TSysCharSet): Boolean;
begin
Результат: = C в CharSet;
конец;

Я думаю, единственная причина, по которой это делается, заключается в том, что P ^ in ['', # 10, # 13, # 0] больше не разрешено в Delphi 2009, поэтому вызов выполняет преобразование типов, чтобы это разрешить.

Тем не менее я очень удивлен этим и до сих пор не доверяю своему результату.

AQTime измеряет что-то неправильно, мне что-то не хватает в этом сравнении, или CharInSet действительно эффективная функция, которую стоит использовать?


Вывод:

Я думаю, ты понял, Барри. Спасибо, что нашли время и сделали подробный пример. Я проверил ваш код на своей машине и получил 0,171, 0,066 и 0,052 секунды (думаю, мой рабочий стол немного быстрее, чем ваш ноутбук).

Тестирование этого кода в AQTime дает: 0,79, 1,57 и 1,46 секунды для трех тестов. Там вы можете увидеть большие накладные расходы от приборов. Но что меня действительно удивляет, так это то, что эти накладные расходы изменяют кажущийся «лучший» результат на функцию CharInSet, которая на самом деле является наихудшим.

Итак, Марку прав, а CharInSet работает медленнее. Но вы непреднамеренно (или, возможно, намеренно) дали мне лучший способ, вытащив то, что делает CharInSet, с помощью метода AnsiChar (P ^) в Set. Помимо небольшого преимущества в скорости по сравнению с методом case, он также требует меньше кода и более понятен, чем использование case.

Вы также сообщили мне о возможности некорректной оптимизации с использованием AQTime (и других инструментальных профилировщиков). Знание этого поможет мне принять решение относительно средств профилирования и анализа памяти для Delphi и это также еще один ответ на мой вопрос Как это делает AQTime?. Конечно, AQTime не меняет код при работе с инструментами, поэтому для этого он должен использовать какое-то другое волшебство.

Итак, ответ в том, что AQTime показывает результаты, которые приводят к неверному выводу.


Продолжение: я оставил этот вопрос с «обвинением» в том, что результаты AQTime могут вводить в заблуждение. Но, честно говоря, я должен посоветовать вам прочитать этот вопрос: Is Существует процедура быстрого получения токенов для Delphi?, которая начинала думать, что AQTime дает вводящие в заблуждение результаты, и пришла к выводу, что это не так.


person lkessler    schedule 02.12.2008    source источник
comment
AQTime действительно изменяет код при работе с инструментами, именно так работают профилировщики инструментов.   -  person Barry Kelly    schedule 02.12.2008
comment
В отличие от большинства профилировщиков, AQTime каким-то образом выполняет свою работу без изменения исходного кода. Мне это очень нравится, потому что не нужно беспокоиться о том, что сбой испортит ваш код. Он должен каким-то образом профилировать объектный код и связывать его со строками исходного кода для анализа. Но я до сих пор не знаю, как это происходит.   -  person lkessler    schedule 02.12.2008
comment
Думаю, я узнал, как это происходит. См .: stackoverflow.com/questions/322315/how-does-aqtime-do-it   -  person lkessler    schedule 03.12.2008


Ответы (5)


AQTime - инструментальный профилировщик. Инструментальные профилировщики часто не подходят для измерения времени кода, особенно в таких микробенчмарках, как ваш, потому что стоимость инструментария часто превышает стоимость измеряемого объекта. С другой стороны, инструменты профилировщика лучше всего подходят для профилирования памяти и других ресурсов.

Профилировщики выборки, которые периодически проверяют расположение ЦП, обычно лучше подходят для измерения времени кода.

В любом случае, вот еще один микробенчмарк, который действительно показывает, что оператор case быстрее, чем CharInSet. Однако обратите внимание, что проверка набора все еще может использоваться с приведением типа для устранения предупреждения об усечении (фактически это единственная причина, по которой существует CharInSet):

{$apptype console}

uses Windows, SysUtils;

const
  SampleString = 'foo bar baz blah de;blah de blah.';

procedure P1;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while not CharInSet(cp^, [#0, ';', '.']) do
    Inc(cp);
end;

procedure P2;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while True do
    case cp^ of
      '.', #0, ';':
        Break;
    else
      Inc(cp);
    end;
end;

procedure P3;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while not (AnsiChar(cp^) in [#0, ';', '.']) do
    Inc(cp);
end;

procedure Time(const Title: string; Proc: TProc);
var
  i: Integer;
  start, finish, freq: Int64;
begin
  QueryPerformanceCounter(start);
  for i := 1 to 1000000 do
    Proc;
  QueryPerformanceCounter(finish);
  QueryPerformanceFrequency(freq);
  Writeln(Format('%20s: %.3f seconds', [Title, (finish - start) / freq]));
end;

begin
  Time('CharInSet', P1);
  Time('case stmt', P2);
  Time('set test', P3);
end.

Его вывод на моем ноутбуке:

CharInSet: 0.261 seconds
case stmt: 0.077 seconds
 set test: 0.060 seconds
person Barry Kelly    schedule 02.12.2008

Барри, я хотел бы отметить, что ваш тест не отражает фактическую производительность различных методов, потому что структура реализаций различается. Вместо этого все методы должны использовать конструкцию while True do, чтобы лучше отразить влияние различных способов выполнения проверки char-in-set.

Вот замена тестовым методам (P2 без изменений, P1 и P3 теперь используют конструкцию while True do):

procedure P1;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while True do
    if CharInSet(cp^, [#0, ';', '.']) then
      Break
    else
      Inc(cp);
end;

procedure P2;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while True do
    case cp^ of
      '.', #0, ';':
        Break;
    else
      Inc(cp);
    end;
end;

procedure P3;
var
  cp: PChar;
begin
  cp := PChar(SampleString);
  while True do
    if AnsiChar(cp^) in [#0, ';', '.'] then
      Break
    else
      Inc(cp);
end;

Моя рабочая станция дает:

CharInSet: 0.099 seconds
case stmt: 0.043 seconds
 set test: 0.043 seconds

Что лучше соответствует ожидаемым результатам. Мне кажется, что использование конструкции case in не очень помогает. Прости, Марко!

person PatrickvL    schedule 02.12.2008
comment
Это хорошее наблюдение по сравнению с конкретными конструкциями. Но код Барри по-прежнему хорош для сравнения возможных реализаций, которые следует сравнивать так, как они были бы написаны. Я хочу, чтобы реализация была самой быстрой. - person lkessler; 02.12.2008
comment
Мне кажется, вы заинтересованы в том, чтобы сознательно ковырять метод set-test, потому что он не похож на оператор case? В любом случае, я не видел этого ответа до сегодняшнего дня, потому что вы никогда не оставляли комментариев к моему ответу :) - person Barry Kelly; 18.12.2008

Здесь можно найти бесплатный профилировщик выборки для Delphi:

https://forums.codegear.com/thread.jspa?messageID=18506

Помимо проблемы некорректного измерения времени инструментальными профилировщиками, следует отметить, что то, что будет быстрее, также будет зависеть от того, насколько предсказуемы ветви «case». Если тесты в «case» имеют одинаковую вероятность встретиться, производительность «case» может оказаться ниже, чем у CharInSet.

person user45336    schedule 11.12.2008
comment
ссылка не работает (не ваша вина :)) - person Z80; 26.02.2018

Код в функции CharInSet быстрее, чем case, время тратится на вызов, используйте while not (cp ^ in [..]), затем

вы увидите, что это пост.

person Community    schedule 05.01.2009

Насколько я знаю, call требует того же количества операций процессора, что и jump, если они оба используют короткие указатели. С длинными указателями может быть иначе. Вызов на ассемблере по умолчанию не использует стек. Если имеется достаточно свободных регистров, используется регистр. Таким образом, операции со стеком также занимают нулевое время. Это просто регистры, что очень быстро.

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

person Din    schedule 02.12.2008