Вообщем добрался до тех исходников и прогнал тесты ещё раз.
Во-первых, что именно тестировалось. В рабочем проекте есть самодельный механизм сериализации, который, помимо всего прочего, до определённых пределов позволяет контролировать версионность хранимых структур, а также вырезает из результирующего XML элементы и целые поддеревья, если в них записаны default'ные значения. К сожалению, выложить сюда рабочий тест не получится, так как этот механизм сильно завязан на внутренние особенности проекта и я с трудом представляю, как можно отцепить всё это в один небольшой бинарник.
Зато в качестве бонуса я получил модульные и интеграционные тесты, которые позволили убедиться, что после всех манипуляций с Option'ами, код оставался рабочим.
Тем не менее, реализацию Option<T> привести проблем не составляет ("лишние" методы опущены):
public abstract class Option<T>: IEnumerable<T> {
public abstract bool HasValue { get; }
public abstract T Value { get; }
#region foreach support
IEnumerator<T> IEnumerable<T>.GetEnumerator() {
if (HasValue)
yield return Value;
else
yield break;
}
IEnumerator IEnumerable.GetEnumerator() {
return (this as IEnumerable<T>).GetEnumerator();
}
#endregion
public Option<V> Map<V>(Func<T, V> func, Func<V, bool> somePredicate) {
if (HasValue)
return func(Value).ToOption(somePredicate);
else
return Option<V>.None;
}
public Option<V> Map<V>(Func<T, V> func) {
return Map(func, Option<V>.DefaultSomePredicate);
}
...
public static Option<T> None {
get { return _noneInstance; }
}
public static Option<T> Some(T value) {
return new Some<T>(value);
}
internal static readonly Func<T, bool> DefaultSomePredicate = value => value != null;
private static readonly Option<T> _noneInstance = new None<T>();
}
public sealed class Some<T>: Option<T> {
public override bool HasValue {
get { return true; }
}
public override T Value {
get { return _value; }
}
internal Some(T value) {
_value = value;
}
private T _value;
}
public sealed class None<T>: Option<T> {
public override bool HasValue {
get { return false; }
}
public override T Value {
get { throw new NoValueException(); }
}
internal None() {
}
}
public static class OptionHelpers {
public static Option<T> ToOption<T>(this T value, Func<T, bool> somePredicate) {
return somePredicate(value) ? Option<T>.Some(value) : Option<T>.None;
}
public static Option<T> ToOption<T>(this T value) {
return ToOption(value, Option<T>.DefaultSomePredicate);
}
}
Насчёт реализации IEnumerable<T> я уже писал выше. Почему DefaultSomePredicate объявлен так хитро — скажу ниже.
В целом эта штука бывает удобна, когда при работе со внешним кодом нужно по-особенному интерпретировать семантику отсутствия значения.
Например, для строки можно одним махом сказать
str.ToOption(_ => !string.IsNullOrEmpty(_));
Во-вторых, как тестировалось.
Для тестов специально был подготовлен относительно толстый (в рамках условий исходной задачи) XML-файл размером 9 метров.
Затем этот файл десериализовывался в объект и сериализовывался обратно. Данная операция для "весомости" повторялась 3 раза.
Для такого цикла замерялось количество затраченного времени, разница в потреблении оперативки до и после, а также количество запусков сборщика мусора на объектах 0-ого поколения. Сразу скажу, что в деталях работы .NET слишком уж сильно не разбираюсь, поэтому где-то мог допсутить оплошность.
В итоге получилось что-то в этом духе:
private static Results RunSerializationCycle() {
var memBefore = GC.GetTotalMemory(true);
var gccBefore = GC.CollectionCount(0);
var watch = Stopwatch.StartNew();
var times = 3;
for (var i = 0; i < times; ++i) {
var element = _serialization.Deserialize(_xml, typeof(Модель));
var xml = _serialization.Serialize(element);
}
var ms = watch.ElapsedMilliseconds;
var gccAfter = GC.CollectionCount(0);
var memAfter = GC.GetTotalMemory(false);
return new Results(ms, memAfter - memBefore, gccAfter - gccBefore);
}
Данный цикл прогонялся требуемое число раз (я решил делать 5) и результаты банально усреднялись. Кроме этого, перед запуском основных циклов прогонялся так называемый "разогревочный" цикл, который должен был взять на себя все затраты по JIT-компиляции, заполнению внутренних кэшей проекта данными, etc.
В-третьих, что менялось.
Первый тест проводился с той реализацией Option, которая приведена в самом начале поста.
Вторая попытка заключалась в устранении наследования и использовании одного единственного класса. По сути, получилось вот что:
public class Option<T>: IEnumerable<T> {
public bool HasValue { get; private set; }
public T Value {
get {
if (!HasValue)
throw new NoValueException();
return _value;
}
}
...
private Option(T value) {
_value = value;
HasValue = true;
}
private Option() {
HasValue = false;
}
}
Третий заход непосредственно гонялся на структурах.
public struct Option<T>: IEnumerable<T> {
public bool HasValue { get; private set; }
public T Value {
get {
if (!HasValue)
throw new NoValueException();
return _value;
}
}
private Option(T value): this() {
_value = value;
HasValue = true;
}
private Option(int dummy): this() {
HasValue = false;
}
}
Пока писал сюда, подумал, что структуры со свойствами и реализующие интерфейсы — это, наверное, не самое приятное зрелище, но я старался по-минимуму ломать существующий код проекта.
Четвёртый вариант подразумевал переписывание алгоритма таким образом, чтобы Option'ы там вообще не использовались.
В-четвёртых, пара лирических отступлений.
VD>> Но "отличались совсем незначительно" скорее всего является следствием синтетичности тесто. По видимому в этих тестах объем работы с option был незначителен по сравнению с основном объемом работы.
Честно говоря, не знаю, как можно достоверно замерить "значительность" работы с Option'ами, но кое-какие цифры всё же приведу.
За один "цикл" проверки создавалось 4.26 миллиона Some и 5.51 миллион None. По результатам профайлинга, количество вызовов методов класса Option сопоставимо только с запросами данных из кэшей.
Ну и обещанный выше финт с DefaultSomePredicate. Признаться, после первого прогона тестов был очень удивлён разницей во времени работы алгоритмов с использованием Option и без него. Разница там была за 50% (в первом посте ошибся немного, прошу прощения
). Причём профайлер говорил, что очень много времени убивается в методе ToOption, но непонятно, где именно. Путём экспериментов с кодом понял, что засада в преобразовании группы методов в объект Func. Если посмотреть на сгенерированный IL, то окажется, что каждого вызова ToOption(value, Option<T>.DefaultSomePredicate) из переданного метода создаётся новый объект типа Func. После этого, оверхэд от использования Option'ов сократился с почти 60% до чуть менее, чем 14%.
Что получилось.
Исходный вариант
Cycle 'Warming up'... 14042; 23775952; 468
Cycle '1'... 13769; 27359464; 489
Cycle '2'... 13864; 30531328; 495
Cycle '3'... 13758; 26056836; 493
Cycle '4'... 13803; 27272640; 496
Cycle '5'... 13792; 25894460; 493
Cycles: 5
Milliseconds: 13797
Bytes: 27422945 (26Mb)
FirstGenCollections: 493
Без наследования
Cycle 'Warming up'... 14069; 24672604; 498
Cycle '1'... 13692; 32547780; 514
Cycle '2'... 13671; 33358004; 522
Cycle '3'... 13652; 33857632; 517
Cycle '4'... 13615; 32919284; 516
Cycle '5'... 13620; 33481480; 517
Cycles: 5
Milliseconds: 13650
Bytes: 33232836 (31Mb)
FirstGenCollections: 517
Структуры
Cycle 'Warming up'... 14364; 26047456; 418
Cycle '1'... 13952; 34068528; 443
Cycle '2'... 13896; 33283716; 435
Cycle '3'... 13901; 32768136; 440
Cycle '4'... 13935; 32473404; 435
Cycle '5'... 13897; 32768200; 440
Cycles: 5
Milliseconds: 13916
Bytes: 33072396 (31Mb)
FirstGenCollections: 438
Без Option'ов
Cycle 'Warming up'... 12419; 18304820; 358
Cycle '1'... 12148; 32868884; 366
Cycle '2'... 12109; 33151904; 364
Cycle '3'... 12092; 33905844; 363
Cycle '4'... 12077; 32660944; 364
Cycle '5'... 12058; 33905844; 363
Cycles: 5
Milliseconds: 12096
Bytes: 33298684 (31Mb)
FirstGenCollections: 364
Как можно увидеть, оверхэд действительно не такой уж и большой, равно как и разница между конкретными реализациями.
Единственное, что хоть как-то отличается — это нагрузка на память и сборщик мусора. Но даже здесь я не вижу у структур какого-то принципиального преимущества.
Если народ скажет, что тесты отстой и ничего не доказывают — с удовольствием попробую покарёжить реализацию ещё немного.
Здравствуйте, Jack128, Вы писали:
J>Здравствуйте, Uriel, Вы писали:
U>>А IEnumerable<T> навеяно реализацией Option'ов в Scala, где их можно пользовать в конструкции for.
U>>Ну и к тому же очень удобно иногда впихивать операции над Option'ами в портянку вызовов LINQ, a-la:
U>>U>>public Option<T> Foo(Bar input);
U>>IEnumerable<T> foo = SomeCollection.SelectMany(_ => Foo(_));
U>>
J>Не лучше ли такой метод использовать:
J>IEnumerable<TResult> Choose<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Option<TResult>> selector) { return source.Select(selector).Where(Option.IsSame); }
J>?
J>Ну и вообще в качестве примера http://msdn.microsoft.com/en-us/library/ee370544.aspx
Можно и такой. Не думаю, что это принципиально что-то изменит. По большому счёту, вся эта пляска с IEnumerable<T> была только ради возможности использования Option'ов в foreach. Которая, в свою очередь, осталась привычкой после изучения Scala.
Все остальные прелести LINQ'а уже чуть позже нашлись.
Здравствуйте, Uriel, Вы писали:
U>Как можно увидеть, оверхэд действительно не такой уж и большой, равно как и разница между конкретными реализациями.
U>Единственное, что хоть как-то отличается — это нагрузка на память и сборщик мусора. Но даже здесь я не вижу у структур какого-то принципиального преимущества.
U>Если народ скажет, что тесты отстой и ничего не доказывают — с удовольствием попробую покарёжить реализацию ещё немного.
Судя по результатам и по рассказу у тебя основной алгоритм занимается массивным выделением памяти. На его фоне оверхэд от опшонов уже мало заметен. Но бывает и другая ситуация. Например, вычислительный алгоритм. Память в нем выделяется редко, а вычислений много. В нем все может быть совсем по другому.