Здравствуйте, уважаемые читатели. Сегодня мы вновь
окунемся в мир Lucene.NET и рассмотрим вопросы производительности RAMDirectory vs
FSDirectory для хранения данных. Небольшое
предисловие к данной статье, чтобы настроить вас на размышление. Если вы
работали с Lucene.NET, то вам
наверняка знаком тот факт, что FSDirectory используется для
хранения информации о документах в индексе на диске, а RAMDirectory используется для хранения информации о документах в RAM памяти. Теперь, собственно, сам вопрос, –
что быстрее будет отрабатывать для сохранения документов: RAMDirectory или же FSDirectory?
Давайте рассмотрим данный пример. Для начала создадим новое консольное
приложение, назвав его “FastSavingInLucene”.
Затем сразу же
поставим сюда через NuGet пакет Lucene.NET для работы с библиотекой для
высокоскоростного поиска Lucene.Net.
Теперь переходим к самому примеру. Не так давно я
прочитал книгу “Lucene in Action”
первого издания (к сожалению, оно очень устарело), для того чтобы понять, как
устроено сохранение индексов, поиск по ним и т.д., так как концепция Lucene там осталась. В итоге нашел там
отличный пример, который написан на Java. Я адаптировал
его на C# и хотел бы привести его вам с объяснением, почему
происходит именно так, а не иначе. Создаем новый класс FsVersusRamDirectory и смотрим на его реализацию ниже.
public class FsVersusRamDirectory
{
#region Variables
private const string LuceneIndexFolder = "LuceneIndex";
private FSDirectory _fsDirectory;
private RAMDirectory _ramDirectory;
private string _dataFolder;
private readonly List<string> _documents = LoadDocument(100000, 5);
#endregion
#region Public Ctor
public FsVersusRamDirectory()
{
InitializeLucene();
}
#endregion
#region Public Properties
public string DataFolder
{
get { return _dataFolder; }
}
public FSDirectory LuceneDirectory
{
get { return _fsDirectory; }
}
public RAMDirectory RamDirectory
{
get { return _ramDirectory; }
}
#endregion
#region Public Methods
public void TestTiming()
{
var ramTiming = TimeIndexerWriter(RamDirectory);
var fsTiming = TimeIndexerWriter(LuceneDirectory);
Console.WriteLine("RAMDirectory Time: {0} ms", ramTiming);
Console.WriteLine("FSDirectory Time: {0} ms", fsTiming);
}
#endregion
#region Private Methods
private void InitializeLucene()
{
_dataFolder = Path.Combine(Environment.CurrentDirectory, LuceneIndexFolder);
var di = new DirectoryInfo(_dataFolder);
if (!di.Exists)
{
di.Create();
}
_fsDirectory = FSDirectory.Open(di.FullName);
_ramDirectory = new RAMDirectory();
}
private static List<string> LoadDocument(int numDocs, int wordsPerDoc)
{
var result = new List<string>();
for (var i = 0; i < numDocs; i++)
{
var doc = new StringBuilder(wordsPerDoc);
for (var j = 0; j < wordsPerDoc; j++)
{
doc.Append("Test");
}
result.Add(doc.ToString());
}
return result;
}
private long TimeIndexerWriter(Lucene.Net.Store.Directory directory)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
AddDocuments(directory);
stopwatch.Stop();
return stopwatch.ElapsedMilliseconds;
}
private void AddDocuments(Lucene.Net.Store.Directory directory)
{
using (
var writer = new IndexWriter(directory, new SimpleAnalyzer(), true,
IndexWriter.MaxFieldLength.UNLIMITED))
{
foreach (var word in _documents)
{
var document = new Document();
var field = new Field("Text", word, Field.Store.NO, Field.Index.NOT_ANALYZED);
document.Add(field);
writer.AddDocument(document);
}
writer.Optimize();
}
}
#endregion
}
Он очень прост для понимания. У нас есть функция LoadDocuments, которая принимает первым параметром
количество документов, вторым – количество слов в нем, а затем создает список
документов и возвращает их. Наружу смотрит только функция TestTiming(), которая и замеряет производительность
для RamDirectory и FSDirectory.
Чтобы запустить этот пример, добавим в наш класс Program
следующий
код.
class Program
{
static void Main(string[] args)
{
var testData = new FsVersusRamDirectory();
testData.TestTiming();
Console.ReadLine();
}
}
Теперь давайте
соберем наш пример в релизе, запустим его несколько раз и посмотрим на то, что
получилось. У меня результат на x64 получился следующим.
Если вы уже плотно
работали с Lucene, то вы наверняка
знаете, в чем причина такого отличия. Если же нет, тогда небольшое объяснение ниже. Этот тест проходит быстрее для FSDirectory, потому что, во-первых, Windows умеет кешировать файлы, а во-вторых, эти файлы реально маленькие. Судя по результатам, может сложиться впечатление, что
RAMDirectory вообще не нужен.
По большому счету, это так и есть, но есть сценарии, в которых RAMDirectory лучше походит, чем FSDirectory, – это unit tests. Дело в том, что
используя RAMDirectory, мы пытаемся обезопасить себя от возможного мусора, связанного с FSDirectory. Поскольку RAMDirectory потеряла свою актуальность для последних версий Lucene.NET, примеры с RAMDirectory оптимизациями убрали со второго издания
книги “Lucene in Action”. Теперь,
разобравшись в RAMDirectory vs FSDirectory, пришло время
рассмотреть вариант, который работает еще быстрее, чем FSDirectory. Представляю вашему вниманию MMapDirectory для x64 приложений. В
Lava для x64 платформ MMapDirectory используется по
умолчанию, если вы используете FSDirectory.open(File). Для Lucene.Net это, к сожалению, не актуально, что вы
можете увидеть по коду ниже.
Единственное, что изменяется, если версия разная, – так
это размер чанков для чтения информации.
Поэтому пока приходится использовать MMapDirectory вручную. Возможно, в более новых версиях Lucene.Net все изменится, и
этот выбор будет автоматическим. MMapDirectory использует мапинг файлов (memory-mapped files) для получения
доступа к индексу. Реализован в C# он криво: обычный копипаст с Java кода, вместо того
чтобы реально заимплементить использование Memory-Mapped Files, доступных в .NET Framework. Использование MMapDirectory в Lucene.NET не очень
популярно. Явно его используют по минимуму, а неявно он не используется.
Поэтому давайте расширим наш предыдущий пример добавлением MMapDirectory и посмотрим после
этого на результат.
public class FsVersusRamDirectory
{
#region Variables
private const string LuceneIndexFolder = "LuceneIndex";
private FSDirectory _fsDirectory;
private MMapDirectory _mMapDirectory;
private RAMDirectory _ramDirectory;
private string _dataFolder;
private readonly List<string> _documents = LoadDocument(100000, 5);
#endregion
#region Public Ctor
public FsVersusRamDirectory()
{
InitializeLucene();
}
#endregion
#region Public Properties
public string DataFolder
{
get { return _dataFolder; }
}
public FSDirectory LuceneDirectory
{
get { return _fsDirectory; }
}
public RAMDirectory RamDirectory
{
get { return _ramDirectory; }
}
public MMapDirectory MMapDirectory
{
get { return _mMapDirectory; }
}
#endregion
#region Public Methods
public void TestTiming()
{
var ramTiming = TimeIndexerWriter(RamDirectory);
var fsTiming = TimeIndexerWriter(LuceneDirectory);
var mMapTiming = TimeIndexerWriter(MMapDirectory);
Console.WriteLine("RAMDirectory Time: {0} ms", ramTiming);
Console.WriteLine("FSDirectory Time: {0} ms", fsTiming);
Console.WriteLine("MMapDirectory Time: {0} ms", mMapTiming);
}
#endregion
#region Private Methods
private void InitializeLucene()
{
_dataFolder = Path.Combine(Environment.CurrentDirectory, LuceneIndexFolder);
var di = new DirectoryInfo(_dataFolder);
if (!di.Exists)
{
di.Create();
}
_fsDirectory = FSDirectory.Open(di.FullName);
_ramDirectory = new RAMDirectory();
_mMapDirectory = new MMapDirectory(di);
}
private static List<string> LoadDocument(int numDocs, int wordsPerDoc)
{
var result = new List<string>();
for (var i = 0; i < numDocs; i++)
{
var doc = new StringBuilder(wordsPerDoc);
for (var j = 0; j < wordsPerDoc; j++)
{
doc.Append("Test");
}
result.Add(doc.ToString());
}
return result;
}
private long TimeIndexerWriter(Lucene.Net.Store.Directory directory)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
AddDocuments(directory);
stopwatch.Stop();
return stopwatch.ElapsedMilliseconds;
}
private void AddDocuments(Lucene.Net.Store.Directory directory)
{
using (
var writer = new IndexWriter(directory, new SimpleAnalyzer(), true,
IndexWriter.MaxFieldLength.UNLIMITED))
{
foreach (var word in _documents)
{
var document = new Document();
var field = new Field("Text", word, Field.Store.NO, Field.Index.NOT_ANALYZED);
document.Add(field);
writer.AddDocument(document);
}
writer.Optimize();
}
}
#endregion
}
Теперь запустим наш
проект несколько раз в релизе и посмотрим на результат. У меня он получился
таким, как показано на рисунке ниже.
Как видим, на MMapDirectory немного быстрее по
времени, чем FSDirectory.
Итоги
Первое, что можно вынести
из данной статьи, – это то, что RAMDirectory не быстрее, чем FSDirectory в большинстве случаев. Второе – MMapDirectory быстрее, чем
базовый FSDirectory. Третье – MMapDirectory не используется
неявно, а использовать его явно вы можете на свой страх и риск, тем более что
все может в любой момент измениться. Чтобы понять, как работает MMapDirectory и в чем его
преимущества и недостатки, рекомендую прочитать статью "Use Lucene’s MMapDirectory on 64bit platforms, please!", для того чтобы все стало на свои места. Статья на английском, но очень
интересная и занимательная, и если вам нравится работать с Lucene, то прочитать ее стоит хотя бы для
того, чтобы расширить немного свой кругозор.