Re[4]: оператор ?. не работает с option
От: Uriel Россия  
Дата: 14.12.11 12:12
Оценка:
Вообщем добрался до тех исходников и прогнал тесты ещё раз.

Во-первых, что именно тестировалось. В рабочем проекте есть самодельный механизм сериализации, который, помимо всего прочего, до определённых пределов позволяет контролировать версионность хранимых структур, а также вырезает из результирующего 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

Как можно увидеть, оверхэд действительно не такой уж и большой, равно как и разница между конкретными реализациями.
Единственное, что хоть как-то отличается — это нагрузка на память и сборщик мусора. Но даже здесь я не вижу у структур какого-то принципиального преимущества.
Если народ скажет, что тесты отстой и ничего не доказывают — с удовольствием попробую покарёжить реализацию ещё немного.
Re[7]: оператор ?. не работает с option
От: Uriel Россия  
Дата: 14.12.11 12:17
Оценка:
Здравствуйте, 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'а уже чуть позже нашлись.
Re[5]: оператор ?. не работает с option
От: VladD2 Российская Империя www.nemerle.org
Дата: 14.12.11 14:25
Оценка: +1
Здравствуйте, Uriel, Вы писали:

U>Как можно увидеть, оверхэд действительно не такой уж и большой, равно как и разница между конкретными реализациями.

U>Единственное, что хоть как-то отличается — это нагрузка на память и сборщик мусора. Но даже здесь я не вижу у структур какого-то принципиального преимущества.
U>Если народ скажет, что тесты отстой и ничего не доказывают — с удовольствием попробую покарёжить реализацию ещё немного.

Судя по результатам и по рассказу у тебя основной алгоритм занимается массивным выделением памяти. На его фоне оверхэд от опшонов уже мало заметен. Но бывает и другая ситуация. Например, вычислительный алгоритм. Память в нем выделяется редко, а вычислений много. В нем все может быть совсем по другому.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.