четверг, 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.

Дальше..

среда, 10 декабря 2008 г.

Баг со структурами и readonly

Сегодня столкнулся с любобытнейшим багом в C#. Вполне возможно, что это не баг, а предусмотренное поведение, но выглядит оно необычно.
В двух словах: readonly переменная изменяема только в конструкторе класса и даже если конструктор вызовет метод, изменяющий ее - эти изменения будут отброшены.


Рассмотрим такой код с вспомогательной структурой и классом.

public struct TestStruct
{
private int m_count;
private int m_value;

public int Count
{
get { return m_count; }
}

public int Value
{
get { return m_value; }
}

public TestStruct(int value)
{
m_value = value;
m_count = 0;
}

public void Increment()
{
m_count++;
}
}

public class TestClass
{
private TestStruct m_struct;

public int Value
{
get { return m_struct.Value; }
}

public int Count
{
get { return m_struct.Count; }
}

public TestClass(int value)
{
m_struct = new TestStruct(value);

for (int i = 0; i < value; i++)
Increment();
}

private void Increment()
{
m_struct.Increment();
}
}


Если создать экземпляр TestClass с каким-либо числом, то значения Value и Count будут равны этому числу. Вполне нормальное и логичное поведение.
Картина меняется, если мы добавим слово readonly:

private readonly TestStruct m_struct;


После этого изменения код

TestClass test = new TestClass(11);
Console.WriteLine("Test result: value {0}, count {1}",
test.Value, test.Count);

выдает такой результат:

Test result: value 11, count 0

Верно такое поведение или нет?
Мы знаем, что структуры являются value-type и для каждого члена структуры, вложенной в класс, память выделяется в самом классе. Потому логично предположить, что readonly распостраняется и на члены структуры. Неприятно, но компилятор нам об этом не сообщает и никак не предупреждает, что этот модификатор приведет к потере данных.
Более того, логично было бы предположить, что если метод Increment() обьявлен как private и используется только в конструкторе, то метод будет inline и на него будут распостранятся те же правила, что и для конструктора. К сожалению, это предположение не оправдывается и нам просто надо помнить, что вызов методов в конструкторе может привести к "необычным" последствиям, не говоря о том, что может быть при вызове виртуальных методов.

Дальше..