четверг, 11 декабря 2008 г.

Поддержка Mono

Сегодня мы начали работать над возобновлением поддержки Mono (http://www.mono-project.com/) платформой RunServer. Причин для этого несколько, но самая первая из них - кросс-платформенность. Три года назад такая мысль меня уже посещала и результат был неутешителен: на Mono 1.14 мы получали примерно десятикратное падение производительности по сравнению с Microsoft .Net Framework 2.0 в RunWoW. Сейчас же различные источники (например этот) сообщают, что скорость Mono вплотную приблизилась к показателям .Net.

После некоторых упражнений с напильником и бубном, RunWoW запустился, но самый первый процесс - загрузка данных из БД - занял около 10 минут против 2х минут в .Net. Вывод напрашивался сам собой, но все-же стало интеерсно, откуда такая существенная разница в скорости.


Первые же тесты показали, что при загрузке небольших таблиц скорость практически идентична, но чем больше возвращается данных, тем больше разрыв. К примеру, 2 и 5 секунд при загрузке 4000 записей против 5 и 20 секунд при загрузке 40000.

Дополнительные проверки показали следующее:
- реализация System.Data.SqlClient в Mono приблизительно на 40% медленнее, чем в . Net;
- время создания обьекта отличается в Mono и .Net на доли процента;
- различные коллекции (связные списки, словари, группы массивов, да и просто обычные списки) на время загрузки данных из БД не влияют, т.к. время выборки, приведения типов, да и вцелом обработки загруженных данных, на порядок больше времени, которое тратится на перебор коллекций;
- в случае, если грузится больше N элементов, выполняется не обычный select * from <..> where <..>, а просто select * from <..> и затем результат фильтруется;

Последняя особенность привлекла мое внимание. С одной стороны, все корректно - я сам писал этот код и отлаживал на различных БД, сравнивая производительность такого решения. С другой стороны оказалось, что если убрать проверку и никогда не загружать все элементы, то разрыв в скорости на Mono и .Net уменьшается до 30-40%.
Копнув чуть глубже я нашел, что сама фильтрация результатов делается не очень оптимально: есть некий массив с ID, которые должны быть в результируещем списке и для каждого элемента таблицы выполняется проверка Array.IndexOf(id) != -1.
Этот метод имеет право на жизнь, если сам IndexOf базируется на каком-нибудь оптимизированном алгоритме (хотя бы на двоичном поиске), но совершенно неприемлим в случае последовательного перебора.

Докопаться до истины стало делом принципа. Я нашел реализацию IndexOf в Mono:

public static int IndexOf (Array array, object value, int startIndex, int count)
{
if (array == null)
throw new ArgumentNullException ("array");

if (array.Rank > 1)
throw new RankException (Locale.GetText ("Only single dimension arrays are supported."));

// re-ordered to avoid possible integer overflow
if (count < 0 || startIndex < array.GetLowerBound (0) || startIndex - 1 > array.GetUpperBound (0) - count)
throw new ArgumentOutOfRangeException ();

int max = startIndex + count;
for (int i = startIndex; i < max; i++) {
if (Object.Equals (value, array.GetValueImpl (i)))
return i;
}

return array.GetLowerBound (0) - 1;
}


Как мы видим, тут имеет место последовательный перебор. Я не стал искать реализацию этого метода в .Net, но подозреваю, что он вызывает Array.BinarySearch, что делает его в разы быстрее. Как бы там ни было, если необходимо проверить наличие записи в коллекции, самым быстрым вариантом является использование Dictionary<>, на котором я и остановился. Результат достаточно приемлим: сервер на Mono потребляет немного больше памяти и загружается на ~40% медленее. К тому же, после отказа от Array.IndexOf и .Net версия стала грузиться быстрее на пару десятков секунд.

Вывод у меня лишь один: Premature optimization is the root of all evil.