Здравствуйте, tyomchick, Вы писали:
T>Часто методы принимают IEnumerable, а передавать хочется всего один элемент. T>Решил написать хелпер: T>Наивным тестами выяснил, что он быстрее чем
… T>Есть ли более быстрый способ?
Ну раз кода тестов вы не привели, то попробуйте пожалуйста сами:
using System;
using System.Collections;
using System.Collections.Generic;
static class Program
{
static void Main() {
}
public static IEnumerable<T> ToEnumerable<T>(T item) {
return new Single<T>(item);
}
}
internal sealed class Single<T> : IEnumerable<T>
{
private readonly T value;
public Single(T value) {
this.value = value;
}
public IEnumerator<T> GetEnumerator() {
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
private sealed class Enumerator : IEnumerator<T>
{
private readonly Single<T> owner;
public Enumerator(Single<T> owner) {
this.owner = owner;
}
private bool IsEnumerated { get; set; }
public T Current {
get { return IsEnumerated ? owner.value : default(T); }
}
// Does not correct (InvalidOperationException does not thrown), but usable.object IEnumerator.Current {
get { return Current; }
}
public bool MoveNext() {
if(IsEnumerated) {
return false;
} else {
IsEnumerated = true;
return true;
}
}
public void Reset() {
IsEnumerated = false;
}
public void Dispose() { }
}
}
Здравствуйте, xy012111, Вы писали:
X>Здравствуйте, tyomchick, Вы писали:
T>>Часто методы принимают IEnumerable, а передавать хочется всего один элемент. T>>Решил написать хелпер: T>>Наивным тестами выяснил, что он быстрее чем X>… T>>Есть ли более быстрый способ?
X>Ну раз кода тестов вы не привели, то попробуйте пожалуйста сами:
Почему то этот ручной вариант самый медленный.
Код кустарного теста:
class Program
{
private const int ITERATION_COUNT = 10000000;
static void Main(string[] args)
{
Console.WriteLine("Тест запущен");
var memoryGC = GC.GetTotalMemory(false);
var memoryWS = Environment.WorkingSet;
var start = Environment.TickCount;
//Test0();
//Test1();
//Test2();
//Test3();
Test4();
Console.WriteLine("Time: {0}, Memory GC: {1}, Memory WS: {2}", Environment.TickCount - start,
GC.GetTotalMemory(false) - memoryGC, Environment.WorkingSet - memoryWS);
Console.ReadLine();
}
private static int Test0()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for (var i = 0; i < ITERATION_COUNT; i++)
value += value;
return value;
}
private static int Test1()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for (var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable1().Sum();
return value;
}
private static int Test2()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for (var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable2().Sum();
return value;
}
private static int Test3()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for (var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable3().Sum();
return value;
}
private static int Test4()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for (var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable4().Sum();
return value;
}
}
public static class Helper
{
public static IEnumerable<T> AsEnumerable1<T>(this T value)
{
yield return value;
}
public static IEnumerable<T> AsEnumerable2<T>(this T value)
{
return Enumerable.Repeat(value, 1);
}
public static IEnumerable<T> AsEnumerable3<T>(this T value)
{
return new[] { value };
}
public static IEnumerable<T> AsEnumerable4<T>(this T value)
{
return new Single<T>(value);
}
}
Здравствуйте, hi_octane, Вы писали:
_>У тебя два класса. Один элемент структурой оборачивать надо.
Кстати, если сочинить такой сам себе итератор:
internal struct Single<T> : IEnumerable<T>, IEnumerator<T>
{
private readonly T _value;
private bool _isEnumerated ;
public Single(T value)
{
_value = value;
_isEnumerated = false;
}
public IEnumerator<T> GetEnumerator()
{
Reset();
return this;
}
IEnumerator IEnumerable.GetEnumerator()
{
Reset();
return this;
}
public T Current
{
get { return _isEnumerated ? _value : default(T); }
}
object IEnumerator.Current
{
get { return Current; }
}
public bool MoveNext()
{
if (_isEnumerated)
{
return false;
}
else
{
_isEnumerated = true;
return true;
}
}
public void Reset()
{
_isEnumerated = false;
}
public void Dispose() { }
}
то я получаю такой результат:
Time: 608, Memory GC: 1301792, Memory WS: 2473984
Т.е. большая нагрузка на кучу сохраняется.
Получается объект всё равно в кучке создается или просто при приведении к интерфейсу всё равно боксинг происходит?
Даже самую простую задачу можно сделать невыполнимой, если провести достаточное количество совещаний
T>Т.е. большая нагрузка на кучу сохраняется. T>Получается объект всё равно в кучке создается или просто при приведении к интерфейсу всё равно боксинг происходит?
В этом случае решарпер показал аж 3 боксинга. После того как я изменил код на использование stopwatch, сделал несколько проходов для устаканивания JIT, добавил GC.Collect, а твой сам-себе итератор поправил на класс чтобы ушли боксинги, получилось примерно так.
Т.е. сам-себе итератор сделанный классом порвал массив. Код под катом.
Скрытый текст
private const int ITERATION_COUNT = 10000000;
static Stopwatch stopwatch = new Stopwatch();
static void Main(string[] args)
{
var tests = new Func<int>[] { Test0, Test1, Test2, Test3, Test4};
for(var i = 0; i < 4; i++)
{
Console.WriteLine("Итерация {0}", i);
for(var j=0; j<tests.Length;j++)
{
RunTest(j, tests[j]);
GC.Collect(0, GCCollectionMode.Forced);
Thread.Sleep(1000);
}
}
//Test0();
//Test1();
//Test2();
//Test3();
//Test4();
}
static void RunTest(int num, Func<int> test)
{
Console.WriteLine("Тест {0} запущен", num);
var memoryGC = GC.GetTotalMemory(false);
var memoryWS = Environment.WorkingSet;
stopwatch.Reset();
stopwatch.Start();
test();
stopwatch.Stop();
Console.WriteLine("Time: {0}, Memory GC: {1}, Memory WS: {2}", stopwatch.ElapsedMilliseconds,
GC.GetTotalMemory(false) - memoryGC, Environment.WorkingSet - memoryWS);
}
private static int Test0()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value;
return value;
}
private static int Test1()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable1().Sum();
return value;
}
private static int Test2()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable2().Sum();
return value;
}
private static int Test3()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable3().Sum();
return value;
}
private static int Test4()
{
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable4().Sum();
return value;
}
}
public static class Helper
{
public static IEnumerable<T> AsEnumerable1<T>(this T value)
{
yield return value;
}
public static IEnumerable<T> AsEnumerable2<T>(this T value)
{
return Enumerable.Repeat(value, 1);
}
public static IEnumerable<T> AsEnumerable3<T>(this T value)
{
return new[] { value };
}
public static IEnumerable<T> AsEnumerable4<T>(this T value)
{
return new Single<T>(value);
}
}
_>>У тебя два класса. Один элемент структурой оборачивать надо. T>Кстати, если сочинить такой сам себе итератор:
Кстати у этого итератора граница применимости — строго однопоточные и однопроходные алгоритмы. Многие стандартные алгоритмы на нём сломаются, например SequenceEqual:
var x = new Single<int>(10);
var t1 = x;
var t2 = x;
Assert.IsTrue(t1.SequenceEqual(t2)); <- упс
Такие грабли по проекту ради миллисекунд я бы не раскладывал.
Здравствуйте, hi_octane, Вы писали: T>>Т.е. большая нагрузка на кучу сохраняется. T>>Получается объект всё равно в кучке создается или просто при приведении к интерфейсу всё равно боксинг происходит? _>В этом случае решарпер показал аж 3 боксинга. После того как я изменил код на использование stopwatch, сделал несколько проходов для устаканивания JIT, добавил GC.Collect, а твой сам-себе итератор поправил на класс чтобы ушли боксинги, получилось примерно так.
Ага, спасибо за нормальный тестер.
Правда на моей рабочей машине результат не стабильный. Судя по всему дело в том, что мой комп беден оперативкой и в ходе теста запускается сборщик мусора. На это указывают и странные показатели по памяти. И тест производительности по сути мало что показывает, т.к. портится запуском сборщика.
Я заменил в этом тесте бесполезный показатель Memory WS, на CollectionCount(0):
Уменьшил число итераций в 200 раз. Запускал с приоритетом "Real Time", для предотвращения переключения контекста процессора. Результат в тиках (stopwatch.ElapsedTicks):
Здравствуйте, hi_octane, Вы писали:
_>>>У тебя два класса. Один элемент структурой оборачивать надо. T>>Кстати, если сочинить такой сам себе итератор:
_>Кстати у этого итератора граница применимости — строго однопоточные и однопроходные алгоритмы.
Да это понятно, я собственно не планировал. Это был экспериент в наивной попытке сделать итератор на стеке.
Даже самую простую задачу можно сделать невыполнимой, если провести достаточное количество совещаний
Здравствуйте, tyomchick, Вы писали: T>то я получаю такой результат:
T>Time: 608, Memory GC: 1301792, Memory WS: 2473984
T>Т.е. большая нагрузка на кучу сохраняется. T>Получается объект всё равно в кучке создается или просто при приведении к интерфейсу всё равно боксинг происходит?
А если — такой:
internal struct SingleEnumerator<T>
{
private readonly T _value;
private bool _isEnumerated ;
public Single(T value)
{
_value = value;
_isEnumerated = false;
}
public T Current
{
get { return _isEnumerated ? _value : default(T); }
}
object IEnumerator.Current
{
get { return Current; }
}
public bool MoveNext()
{
if (_isEnumerated)
{
return false;
}
else
{
_isEnumerated = true;
return true;
}
}
public void Reset()
{
_isEnumerated = false;
}
public void Dispose() { }
}
internal struct SingleEnumerable<T> : IEnumerable<T>, IEnumerator<T>
{
private readonly T _value;
public SingleEnumerable(T value)
{
_value = value;
}
public SingleEnumerator<T> GetEnumerator()
{
return new SingleEnumerator<T>(_value);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
T>>то я получаю такой результат: T>>Time: 608, Memory GC: 1301792, Memory WS: 2473984 T>>Т.е. большая нагрузка на кучу сохраняется. T>>Получается объект всё равно в кучке создается или просто при приведении к интерфейсу всё равно боксинг происходит? S>А если — такой:
S>internal struct SingleEnumerator<T>
Если с помощью этой структуры планируется ускорить изначальный метод
public staticIEnumerable<T> ToEnumerable<T>(this T value)
{
return new[] { value };
}
то боксинг, видится, всё съест. Если поменять сигнаруту на структуру, то конечно будет здорово. Но, кажется, не очень полезно. Или нет?
Здравствуйте, xy012111, Вы писали:
X>Здравствуйте, Sinclair, Вы писали:
X>Если с помощью этой структуры планируется ускорить изначальный метод X>
X>public staticIEnumerable<T> ToEnumerable<T>(this T value)
X>{
X> return new[] { value };
X>}
X>
X>то боксинг, видится, всё съест. Если поменять сигнаруту на структуру, то конечно будет здорово. Но, кажется, не очень полезно. Или нет?
Этот метод не ускорится, но он и не будет вызываться в цикле foreach. Будет вызываться метод, возвращающий структуру.
public SingleEnumerator<T> GetEnumerator()
X>А это вот опечатка? X>
Здравствуйте, xy012111, Вы писали:
X>Если с помощью этой структуры планируется ускорить изначальный метод X>
X>public staticIEnumerable<T> ToEnumerable<T>(this T value)
X>{
X> return new[] { value };
X>}
X>
X>то боксинг, видится, всё съест. Если поменять сигнаруту на структуру, то конечно будет здорово. Но, кажется, не очень полезно. Или нет?
Конечно же имеется в виду поменять сигнатуру на структуру.
Полезность зависит от того, в каком сценарии предполагается использовать сие счастие.
Если в сценарии, где требуется интерфейс — то да, разницы не будет; сплошной боксинг.
А если напрямую в foreach, то компилятор должен убрать боксинг, а джит — всё заинлайнить, выкинуть конструкторы, и оставить только собственно вызов.
Понятное дело, что в человеконаписанном коде всё то же самое можно сделать руками?
foreach(var i in 42.ToEnumerable())
Console.WriteLine(i);
Сие эквивалентно просто Console.WriteLine(42).
А вот если код автогенерится, то ещё есть шансы улучшить поведение. X>А это вот опечатка? X>
Здравствуйте, Sinclair, Вы писали:
X>>Если с помощью этой структуры планируется ускорить изначальный метод
… X>>то боксинг, видится, всё съест. Если поменять сигнаруту на структуру, то конечно будет здорово. Но, кажется, не очень полезно. Или нет? S>Конечно же имеется в виду поменять сигнатуру на структуру. S>Полезность зависит от того, в каком сценарии предполагается использовать сие счастие. S>Если в сценарии, где требуется интерфейс — то да, разницы не будет; сплошной боксинг. S>А если напрямую в foreach, то компилятор должен убрать боксинг, а джит — всё заинлайнить, выкинуть конструкторы, и оставить только собственно вызов.
… S>А вот если код автогенерится, то ещё есть шансы улучшить поведение.
Автогенерить такой неэффективный код — ИМХО проще в генерилке иф сделать на случай одного элемента, чем такие артефакты. Тем более, если от сгенерированного кода ожидается эффективная числодробилка (а иначе и мериться не стоит). Очень-очень узкая область применения. Но да, так будет максимально эффективно (не мерил).
В то время как объединять такой IEnumerable<> (из одного элемента) с другими IEnumerable<> ну мне вот приходится постоянно и тут структура не помогает. Здесь можно схитрить и иметь Single<> классом со структурой-энумератором и сменить сигнатуру метода на Single<>. Тогда и объединение будет дешёвым и итерация (в случае кодогенерации или вообще) не плохой.
Здравствуйте, xy012111, Вы писали:
X>Здравствуйте, samius, Вы писали:
X>>>Если нет, то код вроде как даже не компильнётся. S>>А вроде что помешает?
X>IEnumerator<T> не реализован же. Вместо "IEnumerator<T>" должно было быть по всей видимости "IEnumerable".
Да, теперь вижу что он реализован в SingleEnumerator<T>
Здравствуйте, tyomchick, Вы писали: X>>Ну раз кода тестов вы не привели, то попробуйте пожалуйста сами: T>Почему то этот ручной вариант самый медленный.
Я взял код тестов рядом тут и запустил. Получилось быстрее массива
Немного поменял Single<>, добавив value type итератор там где напрямую Single<> используется.
Но при доступе через IEnumerable<> (как происходит в тестах) используется другой reference type итератор и каплю оптимизднул сам итератор. Но не думаю что оптимизация сильно повлияла.
Тип возвращаемого значения в методах тоже изменил на актуальный — массив в третьем тесте и Single<> в четвёртом. Но это так же на результаты тестов не должно повлиять.
На счёт логирования памяти — не совсем понял что там и как меряется, результаты обсуждать не возьмусь.
Количество итераций увеличил в десять раз до 100M.
Ниже полностью код. Сорри, компилять его надо уже Розлином.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
internal static class Program
{
private const int ITERATION_COUNT = 100000000; // 100Mstatic void Main(string[] args) {
var tests = new Func<int>[] { Test0, Test1, Test2, Test3, Test4, };
for(var index = 0; index < 4; index++) {
Console.WriteLine("Итерация {0}", index);
for(var run = 0; run < tests.Length; run++) {
RunTest(run, tests[run]);
GC.Collect(0, GCCollectionMode.Forced);
Thread.Sleep(1000);
}
}
}
private static void RunTest(int num, Func<int> test) {
Console.WriteLine("Тест {0} запущен", num);
var totalMemoryBefore = GC.GetTotalMemory(false);
var workingSetBefore = Environment.WorkingSet;
var stopwatch = Stopwatch.StartNew();
test();
stopwatch.Stop();
var totalMemoryAfter = GC.GetTotalMemory(false);
var workingSetAfter = Environment.WorkingSet;
Console.WriteLine("Time: {0}, Memory GC: {1}, Memory WS: {2}", stopwatch.ElapsedMilliseconds,
totalMemoryAfter - totalMemoryBefore, workingSetAfter - workingSetBefore);
}
private static int Test0() {
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value;
return value;
}
private static int Test1() {
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable1().Sum();
return value;
}
private static int Test2() {
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable2().Sum();
return value;
}
private static int Test3() {
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable3().Sum();
return value;
}
private static int Test4() {
var value = new Random(DateTime.Now.Millisecond).Next();
for(var i = 0; i < ITERATION_COUNT; i++)
value += value.AsEnumerable4().Sum();
return value;
}
}
internal static class Helper
{
public static IEnumerable<T> AsEnumerable1<T>(this T value) {
yield return value;
}
public static IEnumerable<T> AsEnumerable2<T>(this T value) {
return Enumerable.Repeat(value, 1);
}
public static T[] AsEnumerable3<T>(this T value) {
return new[] { value, };
}
public static Single<T> AsEnumerable4<T>(this T value) {
return new Single<T>(value);
}
}
internal sealed class Single<T> : IEnumerable<T>
{
public Single(T value) {
Value = value;
}
private T Value { get; }
public Enumerator GetEnumerator() => new Enumerator(this);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => new ClassEnumerator(this);
IEnumerator IEnumerable.GetEnumerator() => new ClassEnumerator(this);
public struct Enumerator
{
public Enumerator(Single<T> owner) : this() {
Owner = owner;
}
private Single<T> Owner { get; set; }
public T Current { get; private set; }
public bool MoveNext() {
if(Owner == null) {
return false;
} else {
Current = Owner.Value;
Owner = null;
return true;
}
}
}
private sealed class ClassEnumerator : IEnumerator<T>
{
public ClassEnumerator(Single<T> owner) {
if(owner == null) {
throw new ArgumentNullException(nameof(owner));
}
Owner = owner;
}
private Single<T> Owner { get; set; }
public T Current { get; private set; }
// Does not correct (InvalidOperationException does not thrown), but usable.object IEnumerator.Current => Current;
public bool MoveNext() {
if(Owner == null) {
return false;
} else {
Current = Owner.Value;
Owner = null;
return true;
}
}
public void Reset() {
throw new NotSupportedException();
}
public void Dispose() { }
}
}