понедельник, 21 ноября 2022 г.

TStringList vs. THashedStringList vs. TDictionary

 Пет-проект это называется или просто делать нечего, но надо мне список слов хранить, чтобы проверить слово новое - не значится ли там. Конечно, я слыхал, что THashedStringList ищет вхождение на много быстрее, чем простой TStringList. Но насколько? А ещё, говорят, TDictionary - очень быстрая штука и модная (стильная, молодёжная..). Надо посмотреть.


Взял два списка. В первый список положу слова, которые искать буду во втором. 


  1. procedure TformMain.btnGenerateClick(Sender: TObject);
  2. //
  3. procedure FillList( AStr: TStrings );
  4. begin
  5. AStr.Clear;
  6. AStr.BeginUpdate;
  7. try
  8. var R := Word('Я') - Word( 'А' );
  9. for var N := 1 to 20000 do begin
  10. var W := '1234567';
  11. for var I := 1 to 7 do W[I] := Char( Word( WideChar( 'А' ) ) + Random( R ) );
  12. AStr.Add( W )
  13. end;
  14. finally AStr.EndUpdate;
  15. end;
  16. end;
  17. //
  18. begin
  19. FillList( ListBox1.Items );
  20. FillList( ListBox2.Items );
  21. end;
Мне, так уж складывается, 7 букв в слове достаточно.
Число 20000 подобрал опытным путём. Теперь буду каждое слово помечать "=Yes|No" в зависимости от того найдётся ли оно, и засекать время. Разные способы - разные кнопки:


Прошу прощения, что не использую модные TStopwatch.
  1. procedure TformMain.btnIndexOfClick(Sender: TObject);
  2. //
  3. procedure ClearMarks( AStr: TStrings );
  4. begin
  5. AStr.BeginUpdate;
  6. try
  7. for var I := 0 to AStr.Count-1 do begin
  8. var S := AStr[I];
  9. var P := Pos( '=', S );
  10. if P > 0 then
  11. AStr[I] := Copy( S, 1, P-1 );
  12. end;
  13. finally AStr.EndUpdate;
  14. end;
  15. end;
  16. //
  17. procedure Mark1In2( AStr1, AStr2: TStrings );...
  18. //
  19. procedure Mark1In2Hashed( AStr1, AStr2: TStrings );...
  20. //
  21. procedure Mark1In2Sorted( AStr1, AStr2: TStrings );...
  22. //
  23. procedure Mark1In2Dictio( AStr1, AStr2: TStrings );...
  24. //
  25. begin // TForm16.btnIndexOfClick(Sender: TObject);
  26. Screen.Cursor := crHourGlass;
  27. try
  28. ClearMarks( ListBox1.Items );
  29. var dt := Now;
  30. case TControl( Sender ).Tag of
  31. 1: Mark1In2Hashed( ListBox1.Items, ListBox2.Items );
  32. 2: Mark1In2Sorted( ListBox1.Items, ListBox2.Items );
  33. 3: Mark1In2Dictio( ListBox1.Items, ListBox2.Items );
  34. else Mark1In2 ( ListBox1.Items, ListBox2.Items )
  35. end;
  36. dt := Now - dt;
  37. Caption := FormatDateTime( 'hh:nn:ss.zzz', dt );
  38. finally Screen.Cursor := crDefault;
  39. end;
  40. end;
Начнём с простого (тут подходит поговорка "Простота - хуже воровства") Mark1In2:
  1. procedure Mark1In2( AStr1, AStr2: TStrings );
  2. begin
  3. AStr1.BeginUpdate;
  4. try
  5. for var I := 0 to AStr1.Count-1 do begin
  6. var S := AStr1[I];
  7. AStr1[I] := S + '=' + IfThen( AStr2.IndexOf( S ) >= 0, 'Yes', 'No' );
  8. end;
  9. finally AStr1.EndUpdate;
  10. end;
  11. end;
Остальные методы размножаются копипастой с некоторыми изменениями и дополнениями.
Мой фаворит Mark1In2Hashed достанет из System.IniFiles старый (Delphi 6, если не ошибаюсь) добрый THashedStringList:
  1. procedure Mark1In2Hashed( AStr1, AStr2: TStrings );
  2. begin
  3. AStr1.BeginUpdate;
  4. try
  5. var HSL := THashedStringList.Create;
  6. try
  7. HSL.AddStrings( AStr2 );
  8. for var I := 0 to AStr1.Count-1 do begin
  9. var S := AStr1[I];
  10. AStr1[I] := S + '=' + IfThen( HSL.IndexOf( S ) >= 0, 'Yes', 'No' );
  11. end;
  12. finally HSL.Free;
  13. end;
  14. finally AStr1.EndUpdate;
  15. end;
  16. end;
Но не будем забывать, что и простой
TStringList не лыком шит, имеет в рукаве кое-что:
если его упорядочить, то можно использовать метод половинного деления, а не простой перебор.
И это уже совсем другая история.
  1. procedure Mark1In2Sorted( AStr1, AStr2: TStrings );
  2. begin
  3. AStr1.BeginUpdate;
  4. try
  5. var SL := TStringList.Create( dupIgnore, True, False );
  6. try
  7. SL.AddStrings( AStr2 );
  8. for var I := 0 to AStr1.Count-1 do begin
  9. var S := AStr1[I];
  10. AStr1[I] := S + '=' + IfThen( SL.IndexOf( S ) >= 0, 'Yes', 'No' );
  11. end;
  12. finally SL.Free;
  13. end;
  14. finally AStr1.EndUpdate;
  15. end;
  16. end;
И напоследок рассмотрим данный нам в Generics.Collections список TDictionary.
  1. procedure Mark1In2Dictio( AStr1, AStr2: TStrings );
  2. begin
  3. AStr1.BeginUpdate;
  4. try
  5. var D := TDictionary<string,TObject>.Create;
  6. try
  7. for var I := 0 to AStr2.Count-1 do
  8. D.TryAdd( AStr2[I], nil );
  9. for var I := 0 to AStr1.Count-1 do begin
  10. var S := AStr1[I];
  11. AStr1[I] := S + '=' + IfThen( D.ContainsKey( S ), 'Yes', 'No' );
  12. end;
  13. finally D.Free;
  14. end;
  15. finally AStr1.EndUpdate;
  16. end;
  17. end;
Для тестирования я использовал Embarcadero® Delphi 11 Version 28.0.46481.1287. 
Компьютер у меня не новый, но хороший:
Прежде, чем перейти к результатам тестирования, должен рассказать, что про замечательную
скорость словаря я прочитал в
Программа немецкого товарища, скачанная с его сайта, у меня показывает такие результаты:

TStringList
676: Add: 00:00:08.0883633
676: IndexOf: 00:00:08.0856404
THashedStringList
676: Add: 00:00:08.0273955
676: IndexOf: 00:00:01.6376979
TDictionary
676: Add: 00:00:00.9561740
676: IndexOf: 00:00:00.9005278

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

Я запускал на трёх генерациях списков конкурирующие процедуры  Mark1In2Hashed, Mark1In2Sorted и Mark1In2Dictio одну за одной по три раза, чтобы, не смотря на изменчивую вычислительную среду, видны были чёткие тенденции. 

Замер времени поиска в списке
Среднее
THashedStringList3,1583,1513,1683,2193,0833,1673,1763,0813,1423,149444444
TStringList3,3413,2253,2943,3383,283,2753,2143,3083,2913,285111111
TDictionary3,2393,263,2773,33,253,333,3843,2673,3083,290555556


Как видно, использование всех трёх компонентов приводит к результату от трёх до трёх с половиной секунд. И сверхбыстрый THashedStringList выигрывает у TStringList менее 5%. Хвалёный же TDictionary оказался последним, а не первым. Вот и верь после этого людям!

Но что с "простым" методом Mark1In2? Он работал почти минуту - 58,335 секунд! Я, понятно, не стал его включать в серию испытаний. Но какая это прекрасная иллюстрация  беззаботности - почти минута вместо 3 с хвостиком секунд - разница почти в 20 раз!

Что же из этого следует?
Во-первых, громкое название не говорит о громкой победе: хэш-лист едва-едва выиграл у простого списка, а словарь вообще проиграл. Во-вторых, стоит помнить, что результаты сравнения драматически (вычитал такое слово) зависят от методики испытаний. И сюда же, но всё-таки в-третьих - непонятно, как это у "сумрачного гения" получается так подтасовывать карты?