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

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

4,894,539 казни:

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

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

но такое же количество смертных казней из:

в то время как True do
  случай P^ of
    '', #10, #13, #0: перерыв;
    else inc (P);
  конец;

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

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

добавить edi, $ 02
mov edx, $ 0064b290
movzx еах,[Эди]
вызов CharInSet
тест А1,А1
jz $00649f18 (вернуться к инструкции add)

, тогда как дело логика просто:

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

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

функция CharInSet (C: AnsiChar; const CharSet: TSysCharSet): Boolean;
начать
Результат: = C в кодировке;
конец;

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

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

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


вывод:

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

тестирование этого кода в AQTime, он дает: 0.79, 1.57 и 1.46 секунд для трех тестов. Здесь вы можете увидеть большие накладные расходы от приборов. Но что меня действительно удивляет, так это то, что эти накладные расходы меняют кажущийся "лучший" результат на функцию CharInSet, которая на самом деле является худшей.

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

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

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


Followup: я оставил этот вопрос с "обвинением", что результаты AQTime могут вводить в заблуждение. Но чтобы быть справедливым, я должен направить вас, чтобы прочитать этот вопрос: Есть Ли Быстрая Процедура GetToken Для Delphi? что начал думать, что AQTime дал вводящие в заблуждение результаты, и приходит к выводу, что это не так.

5 ответов


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

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

в любом случае, вот еще одна микробная метка, которая действительно показывает, что a case оператор быстрее, чем CharInSet. Однако обратите внимание, что проверка set все еще может использоваться с typecast для устранения предупреждения усечения (на самом деле это единственная причина, по которой существует 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

Барри, я хотел бы отметить, что ваш бенчмарк не отражает фактическую производительность различных методов, потому что структура реализаций отличается. Вместо этого все методы должны использовать конструкцию "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" на самом деле не помогает. Прости, Марко!


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

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

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


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

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


Как я знаю, вызов занимает такое же количество операций процессора, как и прыжок, если они оба используют короткие указатели. С длинными указателями могут быть разные. По умолчанию Call in assembler не использует stack. При наличии достаточного количества свободных регистров используется регистр. Таким образом, операции стека также занимают нулевое время. Это просто регистры, которые очень быстры.

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