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

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

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

4 894 539 исполнений:

в то время как CharInSet (P ^, ['', # 10, # 13, # 0]) inc (P);

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

Но такое же количество исполнений:

пока True do случай P ^ из
     " ', # 10, # 13, # 0: break;
    else inc (P);
  конец;

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

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

добавить edi, $02
mov edx, $0064b290
movzx eax, [edi]
звоните в CharInSet
test 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, и также проста:

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

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

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

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


Заключение:

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

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

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

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

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


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

4b9b3361

Ответ 1

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

Ответ 2

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

Ответ 3

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

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

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

Ответ 4

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

вы увидите, что это голодание.

Ответ 5

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

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