Бойер-Мур Практика в C#?
Бойер-Мур, пожалуй, самый быстрый неиндексированный алгоритм поиска текста. Так что я внедряю это в C# для моего сайта Black Belt Coder.
У меня было это работает, и он показал примерно ожидаемые улучшения производительности по сравнению с String.IndexOf()
, Тем не менее, когда я добавил StringComparison.Ordinal
аргумент IndexOf
, он начал опережать мою реализацию Бойера-Мура. Иногда на значительную сумму.
Интересно, кто-нибудь может помочь мне понять, почему. Я понимаю почему StringComparision.Ordinal
может ускорить процесс, но как это может быть быстрее, чем Бойер-Мур? Это из-за накладных расходов самой платформы.NET, возможно, потому, что индексы массива должны быть проверены, чтобы убедиться, что они находятся в диапазоне, или что-то еще вообще. Некоторые алгоритмы просто не практичны в C#.NET?
Ниже приведен код ключа.
// Base for search classes
abstract class SearchBase
{
public const int InvalidIndex = -1;
protected string _pattern;
public SearchBase(string pattern) { _pattern = pattern; }
public abstract int Search(string text, int startIndex);
public int Search(string text) { return Search(text, 0); }
}
/// <summary>
/// A simplified Boyer-Moore implementation.
///
/// Note: Uses a single skip array, which uses more memory than needed and
/// may not be large enough. Will be replaced with multi-stage table.
/// </summary>
class BoyerMoore2 : SearchBase
{
private byte[] _skipArray;
public BoyerMoore2(string pattern)
: base(pattern)
{
// TODO: To be replaced with multi-stage table
_skipArray = new byte[0x10000];
for (int i = 0; i < _skipArray.Length; i++)
_skipArray[i] = (byte)_pattern.Length;
for (int i = 0; i < _pattern.Length - 1; i++)
_skipArray[_pattern[i]] = (byte)(_pattern.Length - i - 1);
}
public override int Search(string text, int startIndex)
{
int i = startIndex;
// Loop while there's still room for search term
while (i <= (text.Length - _pattern.Length))
{
// Look if we have a match at this position
int j = _pattern.Length - 1;
while (j >= 0 && _pattern[j] == text[i + j])
j--;
if (j < 0)
{
// Match found
return i;
}
// Advance to next comparision
i += Math.Max(_skipArray[text[i + j]] - _pattern.Length + 1 + j, 1);
}
// No match found
return InvalidIndex;
}
}
РЕДАКТИРОВАТЬ: я разместил весь свой тестовый код и выводы по этому вопросу на http://www.blackbeltcoder.com/Articles/algorithms/fast-text-search-with-boyer-moore.
3 ответа
Основываясь на моих собственных тестах и комментариях, сделанных здесь, я пришел к выводу, что причина String.IndexOf()
так хорошо работает с StringComparision.Ordinal
потому что метод вызывает неуправляемый код, который, вероятно, использует оптимизированный вручную язык ассемблера.
Я провел ряд различных тестов и String.IndexOf()
кажется, что это быстрее, чем что-либо, что я могу реализовать с помощью управляемого кода C#.
Если кому-то интересно, я написал все, что узнал об этом, и опубликовал несколько вариантов алгоритма Бойера-Мура на C# по адресу http://www.blackbeltcoder.com/Articles/algorithms/fast-text-search-with-boyer-moore.
Держу пари, что установка этого флага позволяет String.IndexOf использовать самого Бойера-Мура. И его реализация лучше вашей.
Без этого флага следует осторожно использовать Бойера-Мура (и, вероятно, нет) из-за потенциальных проблем с Unicode. В частности, возможность Unicode приводит к взрыву таблиц переходов, которые использует Бойер-Мур.
Я внес небольшие изменения в ваш код и сделал другую реализацию алгоритма Бойера-Мура и получил лучшие результаты. Я получил идею для этой реализации отсюда
Но, честно говоря, я ожидал бы достичь более высокой скорости по сравнению с
IndexOf
.
class SearchResults
{
public int Matches { get; set; }
public long Ticks { get; set; }
}
abstract class SearchBase
{
public const int InvalidIndex = -1;
protected string _pattern;
protected string _text;
public SearchBase(string text, string pattern) { _text = text; _pattern = pattern; }
public abstract int Search(int startIndex);
}
internal class BoyerMoore3 : SearchBase
{
readonly byte[] textBytes;
readonly byte[] patternBytes;
readonly int valueLength;
readonly int patternLength;
private readonly int[] badCharacters = new int[256];
private readonly int lastPatternByte;
public BoyerMoore3(string text, string pattern) : base(text, pattern)
{
textBytes = Encoding.UTF8.GetBytes(text);
patternBytes = Encoding.UTF8.GetBytes(pattern);
valueLength = textBytes.Length;
patternLength = patternBytes.Length;
for (int i = 0; i < 256; ++i)
badCharacters[i] = patternLength;
lastPatternByte = patternLength - 1;
for (int i = 0; i < lastPatternByte; ++i)
badCharacters[patternBytes[i]] = lastPatternByte - i;
}
public override int Search(int startIndex)
{
int index = startIndex;
while (index <= (valueLength - patternLength))
{
for (int i = lastPatternByte; textBytes[index + i] == patternBytes[i]; --i)
{
if (i == 0)
return index;
}
index += badCharacters[textBytes[index + lastPatternByte]];
}
// Text not found
return InvalidIndex;
}
}
Измененный код из
Form1
:
private void RunSearch(string pattern, SearchBase search, SearchResults results)
{
var timer = new Stopwatch();
// Start timer
timer.Start();
// Find all matches
int pos = search.Search(0);
while (pos != -1)
{
results.Matches++;
pos = search.Search(pos + pattern.Length);
}
// Stop timer
timer.Stop();
// Add to total Ticks
results.Ticks += timer.ElapsedTicks;
}