Delphi: Почему оператор сравнения бинарных строк (=) не использует SameStr?

Общеизвестно, что SameStr(S1, S2) быстрее, чем S1 = S2, где var S1, S2: string в Delphi.

(И, конечно, SameText(S1, S2) намного быстрее, чем AnsiLowerCase(S1) = AnsiLowerCase(S2).)

Но, насколько я понимаю, SameStr(S1, S2) делает то же самое, что и S1 = S2, поэтому я не могу не задаться вопросом, почему в мире компилятор Delphi не использует код SameStr, когда он тестирует для равенства строк с помощью оператора =. Неужели для этого должна быть причина?

Некоторые бенчмаркинга

Тривиальная программа,

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  RejbrandCommon;

const
  N = 1000000;

var
  Strings1, Strings2: StringArray;
  i: integer;
  b: {dummy }boolean;

procedure CreateRandomStringArrays;
var
  i: integer;
begin
  SetLength(Strings1, N);
  SetLength(Strings2, N);
  for i := 0 to N - 1 do
  begin
    Strings1[i] := RandomString(0, 40);
    Strings2[i] := RandomString(0, 40);
  end;
end;

begin

  CreateRandomStringArrays;

  StartClock;
  for i := 0 to N - 1 do
    if Strings1[i] = Strings2[i] then
      b := not b;
  StopClock;
  OutputClock;

  StartClock;
  for i := 0 to N - 1 do
    if SameStr(Strings1[i], Strings2[i]) then
      b := not b;
  StopClock;
  OutputClock;

  Pause;

end.

где

function RandomString(const LowerLimit: integer = 2; const UpperLimit: integer = 20): string;
var
  N, i: integer;
begin
  N := RandomRange(LowerLimit, UpperLimit);
  SetLength(result, N);
  for i := 1 to N do
    result[i] := RandomChar;
end;

и встроенный

function RandomChar: char;
begin
  result := chr(RandomRange(ord('A'), ord('Z')));
end;

и функции "clock" просто завершают QueryPerformanceCounter, QueryPerformanceFrequency и Writeln, выводят вывод

2.56599325762716E-0002
1.24310093156453E-0002
ratio ~ 2.06

Если разница в длине двух строк, которые мы сравниваем, велика, то разница еще больше. Мы стараемся

Strings1[i] := RandomString(0, 0); // = '';
Strings2[i] := RandomString(0, 40);

и получим

1.81630411160156E-0002
4.44662043198641E-0003
ratio ~ 4.08

Итак, почему компилятор не использует код SameStr при записи сборки для S1 = S2?

Update

После прочтения прекрасного ответа Cosmin Prund, я не мог устоять перед установкой

Strings1[i] := RandomString(40, 40);
Strings2[i] := RandomString(40, 40);

для создания строк одинаковой длины и действительно.

2.74783364614126E-0002
1.96818773095322E-0002
ratio ~ 1.40

Hm... SameStr все еще выигрывает...

Мои спецификации

CPU Brand String: Intel(R) Core(TM) i7 CPU         870  @ 2.93GHz
Memory: 6 GB
OS: Windows 7 Home Premium (64-bit)
Compiler/RTL: Delphi 2009

Update

Казалось бы (см. комментарии ниже Cosmin Prund отличный ответ), так как оператор = был изменен между D2009 и D2010. Кто-нибудь может это подтвердить?

Ответы

Ответ 1

Ответ

Все зависит от того, как вы создаете случайные строки. Я использовал модифицированную версию кода, потому что очень немногие из нас имеют подразделение RejbrandCommon, и потому, что я хотел использовать Excel для завершения моих анализов (и сделать красивые картинки).

Код (пропустите код, чтобы увидеть некоторые выводы):

программа Project3;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

const
  StringsNumber = 2000000;

var
  Strings1, Strings2: array of string;
  StrLen: integer;
  b: {dummy }boolean;

function RandomString(MinLen, MaxLen:Integer):string;
var N, i:Integer;
begin
  N := MinLen + Random(MaxLen-MinLen);
  Assert(N >= MinLen); Assert(N <= MaxLen);
  SetLength(Result, N);
  for i:=1 to N do
    Result[i] := Char(32 + Random(1024)); // Random Unicode Char
end;

procedure CreateRandomStringArrays(StrLen:Integer);
var
  i: integer;
  StrLen2:Integer;
begin
  SetLength(Strings1, StringsNumber);
  SetLength(Strings2, StringsNumber);
  for i := 0 to StringsNumber - 1 do
  begin
    StrLen2 := StrLen + Random(StrLen div 2);
    Strings1[i] := RandomString(StrLen, StrLen2);
    StrLen2 := StrLen + Random(StrLen div 2);
    Strings2[i] := RandomString(StrLen, StrLen2);
  end;
end;

var C1, C2, C3, C4:Int64;

procedure RunTest(StrLen:Integer);
var i:Integer;
begin
  CreateRandomStringArrays(StrLen);

  // Test 1: using equality operator
  QueryPerformanceCounter(C1);
  for i := 0 to StringsNumber - 1 do
    if Strings1[i] = Strings2[i] then
      b := not b;
  QueryPerformanceCounter(C2);

  // Test 2: using SameStr
  QueryPerformanceCounter(C3);
  for i := 0 to StringsNumber - 1 do
    if SameStr(Strings1[i], Strings2[i]) then
      b := not b;
  QueryPerformanceCounter(C4);

  // Results:
  C2 := C2 - C1;
  C4 := C4 - C3;
  WriteLn(IntToStr(StrLen) + #9 + IntToStr(C2) + #9 + IntToStr(C4));
end;

begin

  WriteLn('Count'#9'='#9'SameStr');
  for StrLen := 1 to 50 do
    RunTest(StrLen);

end.

Я выполнил процедуру CreateRandomStringArrays, взяв параметр StrLen, чтобы я мог запускать несколько аналогичных тестов в цикле. Я сам использовал код QueryPerformanceCounter, а WriteLn - результат в формате с разделителями табуляции, чтобы я мог скопировать/вставить его в Excel. В Excel я получаю результаты в этой форме:

StrLen  =   SameStr
1   61527   69364
2   60188   69450
3   72130   68891
4   78847   85779
5   77852   78286
6   83612   88670
7   93936   96773

Затем я немного изменил ситуацию. На каждой строке задано максимальное значение "1", а другое значение - от 1. Результат выглядит следующим образом:

StrLen  =   SameStr
1   0,88    1
2   0,86    1
3   1   0,95
4   0,91    1
5   0,99    1
6   0,94    1
7   0,97    1

И затем я начал играть с помощью процедуры CreateRandomStringArrays для запуска нескольких тестов.

Вот как выглядит сюжет для исходного случая (CreateRandomStringArrays генерирует строки случайной длины длиной 1 на любую ось X). Синий - результат для оператора "=", красный - результат для "SameStr", ниже - лучше. Он ясно показывает, что SameStr() имеет край для строк длиннее 10 символов.

alt text http://fisiere.sediu.ro/PentruForumuri/V1_1_to_maxlen.png

Следующий тест, сделанный CreateRandomStringArrays, возвращает строки длины EQUAL. Содержимое строк все еще полностью случайное, но длина строк равна тому, что есть на оси X. На этот раз оператор "=" явно более эффективен:

alt text http://fisiere.sediu.ro/PentruForumuri/V1_equal_strings.png

Теперь реальный вопрос: с REAL-кодом, какова вероятность того, что строки будут равны? И насколько велика должна быть разница для SameStr(), чтобы начать набирать рельеф? Следующий текст, я строю две строки, первую из StrLen (число на оси X), вторая строка имеет длину StrLen + Random (4). Опять же, оператор "=" лучше:

alt text http://fisiere.sediu.ro/PentruForumuri/V1_rnd_p4.png

Следующий тест, у меня две строки, каждая из которых: StrLen + Random (StrLen div 10). Оператор "=" лучше.

alt text http://fisiere.sediu.ro/PentruForumuri/V1_rnd_pm_10p.png

... и мой последний тест, строки +/- 50% длины. Формула: StrLen + Random (StrLen div 2). SameStr() выигрывает этот раунд:

alt text http://fisiere.sediu.ro/PentruForumuri/V1_rnd_pm_50p.png

Заключение

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

Ответ 2

SameStr имеет дополнительный третий параметр: LocaleOptions. Вы получаете поведение, подобное "=", оставляя третий параметр: независимое сравнение локального значения senstive.

Вы думаете, что это то же самое, что и двоичное сравнение, но это не так.

Так как строки D2009 Delphi содержат полезную нагрузку "кодовая страница" в дополнение к длине и пересчету.

  StrRec = packed record
    codePage: Word;
    elemSize: Word;
    refCnt: Longint;
    length: Longint;
  end;

Когда вы выполняете String1 = String2, вы сообщаете компилятору игнорировать всю информацию о строке и просто выполнять двоичное сравнение (для этого используется UStrEqual).

Когда вы делаете SameStr или CompareStr (который используется SameStr), Delphi сначала проверит строку для Unicode (UTF-16LE), а если нет, преобразуйте их перед выполнением фактической работы.

Это можно увидеть, когда вы посмотрите на реализацию CompareStr (тот, у кого нет третьего параметра), который после первоначальных оптимизаций проверяет, являются ли аргументы строками unicode, а если нет, преобразует их с помощью UStrFromLStr.

Update:

Фактически, UStrEqual (посредством UStrCmp) также выполняет преобразования, такие как CompareStr, он смотрит на elemSize строк, чтобы решить, являются ли они Unicode или нет, и преобразует их, если они не являются.

Поэтому причина, по которой компилятор не использует SameStr (CompareStr) для оператора =, ускользает от меня в данный момент. Единственное, о чем я могу думать, это то, что он имеет хорошую аналогию с LStrEqual, используемым для '=' - сравнить AnsiStrings. Я думаю, что знают только компиляторы.

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

Ответ 3

В моей системе "=" быстрее, чем SameStr.

SameStr становится быстрее (около 20%) с примером "RandomString (0,0)". но опять же, если это вторая строка, которая установлена ​​в '', показатели почти одинаковы. После еще нескольких тестов кажется, что разница в длине не влияет на производительность, это пустая строка, которая делает.

Cosmin Prund только что опубликовал гораздо более тщательный анализ...

Следует иметь в виду, что для функций, которые являются такими маленькими ( 1 миллион тестов за несколько мсек), фактический процессор, работающий с кодом, может иметь большое значение. Код ASM может быть немного более дружелюбным к BPU из 1 процессора, чем другой... Или некоторые инструкции могут работать более эффективно на разных CPU. Возможно, это повлияет на выравнивание данных. Ошибка кэша. Это всего лишь несколько примеров на аппаратном уровне, которые могут повлиять на конечную производительность.

Для справки, тесты, которые я делал, были на процессоре Phenom X4.

Ответ 4

В будущем будут продолжены некоторые тесты:

  • Обновление Delphi Seattle 1
  • i5-2500k @4.3Ghz
  • 1 миллиард итераций
  • сравнение 2 строк, длиной 17 символов

Различные тексты:
//= → 1890 мс
//CompareText → 4500 мс
//CompareStr → 2130 мс

Тот же текст:
//= → 1890 мс
//CompareText → 10900 ms
//CompareStr → 1895 мс

Заключение: = быстрее во всех случаях, однако CompareStr с тем же текстом почти так же быстро, как =. Кроме того, CompareText/Str кажется MUCH медленнее при работе с Ansi-строками.