Здравствуйте, Sinix, Вы писали:
S>И при этом чужой опыт хождения по граблям ты отметаешь не глядя
Наоборот, я в это обсуждение влез только потому, что сам несколько лет назад обыгрывал точно такие же сценарии в дотнете.
S>Ну, т.е. чтоб хоть как-то нормально обсуждать, надо сначала расписать совсем базовые вещи, да ещё и добиться, чтоб ты с ними согласился, а не продолжал гнуть своё. Вот это-то мне как раз делать и влом.
Не надо ничего расписывать словами.
Я тебе показал в листинге, что твой тест не тестирует того, что ты планировал тестировать.
Остальное — лирика.
Здравствуйте, Sinix, Вы писали:
S>Смысл в следующем: в RyuJit одно поломали другое починили здорово подкрутили эвристики для loop hoisting, т.е. то самое вытаскивание обращения к полю за тело цикла. S>И, как результат, для вещей, которые JIT умеет оптимизировать (обсуждаемый случай — как раз из них)
В общем случае такие оптимизации невозможны.
Они как раз, сцуко, только в синтетических тестах и происходят.
Это по опыту ежедневной работы с куда как более мощным офлайн-оптимизатором С++, который еще больше кода выкидывает нафик.
Насколько я понял, BenchmarkRunner создаёт выделенную сборку для каждого теста, куда включает только тестируемый метод и зависимые от него.
Поэтому, тестировать такие вещи через BenchmarkRunner нельзя.
Надо писать ручной тест и в коде сделать несколько "отвлекающих манёвров", например, симулировать доступ к тестируемому объекту из разных потоков, но во время выполнения тела теста объект не трогать. Тогда джит не сможет сделать никаких предположений и вынужден будет обращаться к поля объекта "честно", т.е. без кеширования их в начале цикла. Потому что именно такова семантика средней программы.
S>смысла заморачиваться с структурой нет — получаем 5%-10% выигрыша
Выигрыш там в 3-4 раза, это на реальных дотнетных проектах (VoIP, HFT).
В синтетических тестах выигрыш всегда чуть меньше выходит — примерно в 2.5 раза.
Потому что синтетический тест измеряет профит лишь от одного уровня вложенности.
Если таких уровней вложенности более одного, то профит умножается для каждого уровня.
В среднем проекте обычно присутствует некий высокоуровневый-объект-обертка над неким List<>, тот в свою очередь тоже является оберткой над Array. Т.е. уровень косвенности минимум 2, теоретический прирост должен быть в ~6.25 раз, но фактически помимо беготни по графу происходят еще вычисления, поэтому реальный эффект на боевых проектах где-то 3-4 раза.
В нейтиве такие же цифры получаются, что характерно.
Т.е. дотнет тут не на уникальной позиции.
S>в лучшем случае в обмен на лишний боксинг в типовом случае работы с linq.
Лишний боксинг тоже зло, ес-но.
Именно поэтому я предлагал рисовать свои "точки входа" для классов-расширений типа Enumerable.
Что там будет происходить с value-типами-коллекциями в Linq-выражениях в этом его expression-синтаксисе, если честно, не в курсе, еще не экспериментировал. Что-то мне подсказывает, что там, где производительность важна, там этот синтаксис не используют.
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, samius, Вы писали:
S>>Перечислитель будет доступен лишь через методы интерфейса IEnumerator<int>. А значит — все равно отбоксится.
V>Не отбоксится. ))
Ты прав, не отбоксится. Метод, возвращающий struct Enumerator из твоего Sum даже не будет вызван. Так чего ради нужна такая "оптимизация"?
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, samius, Вы писали:
V>А рядом для static double Sum<...> where TEnumerator : IEnumerator<double>? ))
Если мне понадобится такая фигня ради того что бы ехать, то я не побрезгую назвать методы SumInt и SumDouble.
Здравствуйте, samius, Вы писали:
V>>А рядом для static double Sum<...> where TEnumerator : IEnumerator<double>? )) S>Если мне понадобится такая фигня ради того что бы ехать, то я не побрезгую назвать методы SumInt и SumDouble.
Здравствуйте, samius, Вы писали:
S>>>Перечислитель будет доступен лишь через методы интерфейса IEnumerator<int>. А значит — все равно отбоксится. V>>Не отбоксится. )) S>Ты прав, не отбоксится. Метод, возвращающий struct Enumerator из твоего Sum даже не будет вызван. Так чего ради нужна такая "оптимизация"?
Ради уменьшения уровня косвенности:
Method | Mean | StdDev | Scaled | Scaled-StdDev |
------------------ |-------------- |---------- |------- |-------------- |
TestRefWrapper | 2,345.5918 us | 7.4546 us | 1.00 | 0.00 |
TestStructWrapper | 961.5590 us | 6.0217 us | 0.41 | 0.00 |
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, samius, Вы писали:
S>>>>Перечислитель будет доступен лишь через методы интерфейса IEnumerator<int>. А значит — все равно отбоксится. V>>>Не отбоксится. )) S>>Ты прав, не отбоксится. Метод, возвращающий struct Enumerator из твоего Sum даже не будет вызван. Так чего ради нужна такая "оптимизация"?
V>Ради уменьшения уровня косвенности: V>
V> Method | Mean | StdDev | Scaled | Scaled-StdDev |
V>------------------ |-------------- |---------- |------- |-------------- |
V> TestRefWrapper | 2,345.5918 us | 7.4546 us | 1.00 | 0.00 |
V> TestStructWrapper | 961.5590 us | 6.0217 us | 0.41 | 0.00 |
V>
V>Разница ~2.5 раза.
При этом у тебя для 64
Результаты для x64 :
Method | Mean | StdDev | Scaled | Scaled-StdDev |
------------------ |-------------- |---------- |------- |-------------- |
TestRefWrapper | 816.8624 us | 3.9044 us | 1.00 | 0.00 |
TestStructWrapper | 774.8507 us | 1.6223 us | 0.95 | 0.00 |
Никаких ~2.5 раза.
Попробуй .Net Core
и солнце б утром не вставало, когда бы не было меня
Здравствуйте, Serginio1, Вы писали:
V>>Садись, два. V>>Для x64 исходный тест не работоспособен. S>Так это твои результаты http://rsdn.org/forum/dotnet/6731352.1
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, samius, Вы писали:
S>>Ты прав, не отбоксится. Метод, возвращающий struct Enumerator из твоего Sum даже не будет вызван. Так чего ради нужна такая "оптимизация"?
V>Ради уменьшения уровня косвенности: V>
V> Method | Mean | StdDev | Scaled | Scaled-StdDev |
V>------------------ |-------------- |---------- |------- |-------------- |
V> TestRefWrapper | 2,345.5918 us | 7.4546 us | 1.00 | 0.00 |
V> TestStructWrapper | 961.5590 us | 6.0217 us | 0.41 | 0.00 |
V>
V>Разница ~2.5 раза.
За счет чего разница будет в 2.5 раза, если ты уменьшил уровень косвенности лишь у одного вызова на забег по коллекции, в то время как 2*n вызовов осталось ровно с той же косвенностью, как и при передаче интерфейса?
Здравствуйте, vdimas, Вы писали:
V>В общем случае такие оптимизации невозможны. V>В среднем проекте обычно присутствует некий высокоуровневый-объект-обертка над неким List<>, тот в свою очередь тоже является оберткой над Array. Т.е. уровень косвенности минимум 2, теоретический прирост должен быть в ~6.25 раз, но фактически помимо беготни по графу происходят еще вычисления, поэтому реальный эффект на боевых проектах где-то 3-4 раза.
Угу, кажись, я наконец понял про что ты говоришь.
Речь про попытку совместить обработку кучи однородных данных в hotpath и сложные структуры данных над plain-массивом, так?
Как-то совсем частный сценарий получается. Если честно, я бы такое вытаскивал на плюсы сразу. По крайней мере пока нормальный оптимизатор не завезут.
В дотнете как правило в hotpath используется массив напрямую. Именно чтоб не напарываться на "а сегодня JIT не смог".
Но это справедливо именно для тшательно вылызанного perf-critical кода. Для всего остального никто с тонной вложенных структур заморачиваться не будет, возвращаемся к результатам выше
Здравствуйте, samius, Вы писали:
V>>Ради уменьшения уровня косвенности: V>>
V>> Method | Mean | StdDev | Scaled | Scaled-StdDev |
V>>------------------ |-------------- |---------- |------- |-------------- |
V>> TestRefWrapper | 2,345.5918 us | 7.4546 us | 1.00 | 0.00 |
V>> TestStructWrapper | 961.5590 us | 6.0217 us | 0.41 | 0.00 |
V>>
V>>Разница ~2.5 раза.
S>За счет чего разница будет в 2.5 раза, если ты уменьшил уровень косвенности лишь у одного вызова на забег по коллекции, в то время как 2*n вызовов осталось ровно с той же косвенностью, как и при передаче интерфейса?
В этой таблице результаты джита x86. Очевидно, что этот джит не провел для тестового цикла оптимизацию, т.е. не выкинул из его тела обращение к полям объекта "честным образом", т.е. через переменную, указанную в исходнике теста.
Здравствуйте, Sinix, Вы писали:
S>Угу, кажись, я наконец понял про что ты говоришь. S>Речь про попытку совместить обработку кучи однородных данных в hotpath и сложные структуры данных над plain-массивом, так?
Почти так. Дело не только в массиве.
Если объекты агрегируют друг друга, то в плюсах это обычно происходит естественным образом — по значению.
Отсюда "бесплатность" абстракций.
По указателю/ссылке обычно описывается отношение ассоциации, когда некий объект расшарен м/у несколькими.
Ну или когда некий объект предназначен для передачи в другой поток.
S>Как-то совсем частный сценарий получается.
Это общий сценарий. Я как-то специально подсчитывал, у меня на сотни прикладных классов было только 5 объектов верхнего уровня, создаваемых явно по new и всего два объекта для передачи их м/у потоками.
Т.е., когда такая возможность располагать объекты в памяти друг друга идёт изкаробки, то ей активно пользуются при разработке архитектуры, ес-но.
S>В дотнете как правило в hotpath используется массив напрямую. Именно чтоб не напарываться на "а сегодня JIT не смог".
Угу, в Джава тоже.
Причем, в дотнете хоть сопряжение с нейтивом, считай, бесплатное, поэтому такой код, действительно, часто пишут как внешний или линкуемый модуль на плюсах. А в Джаве осилить JNI — тот еще трах и не всегда даёт профит, особенно при частых вызовах. Поэтому берут массив байт и ручным образом в нем рисуют сложные структуры (для HFT, например), т.е. ручками выполняют работу некоего нейтивного компилятора. ))
S>Но это справедливо именно для тшательно вылызанного perf-critical кода. Для всего остального никто с тонной вложенных структур заморачиваться не будет, возвращаемся к результатам выше
Ну да. Считанные единицы объектов верхнего уровня могут быть не заморочены. ))
Здравствуйте, vdimas, Вы писали:
V>1. Методы-расширения, как и обычные методы, не включают в свою сигнатуру ограничения.
Ок, но это не относится к тезису «вообще таких алгоритмов мало, которые можно выразить в дотнете для value и ref-типов в генериках и они будут корректно работать в обоих случаях.»
Давай рассмотрим конкретную (в меру извращённую) реализацию упомянутого суммирования, с дженериком и констрейнтами:
internal static class Foo<TElement>
{
internal static TElement Sum<TEnumerable, TMonoid>(TEnumerable items, TMonoid monoid)
where TEnumerable : IEnumerable<TElement>
where TMonoid : IMonoid<TElement>
{
// При сравнении объекта value-типа с литералом `null`
// не будет боксинга в рантайме согласно C# Specification, section 7.10.6.if (items == null)
throw new ArgumentNullException(nameof(items));
if (monoid == null)
throw new ArgumentNullException(nameof(monoid));
TElement result = monoid.Identity;
foreach (var item in items)
result = monoid.Combine(result, item);
return result;
}
}
Почему этот алгоритм будет работать для reference-типов и не будет для value-типов, или наоборот?
V>3. Из-за п.2. в теле обычных и генерик-методов может случиться неявный боксинг структуры, а программист и не обратит внимания. Но результат боксинга структуры никогда не равен null, даже если структура не инициализирована.
Если алгоритму передают некорректный экземпляр value-типа, то алгоритм поведёт себя так же, как если бы ему подали по не-null ссылке некорректный экземпляр reference-типа.
Алгоритму могут передать объект значимого типа, который бросит `System.InvalidOperationException` в методе `GetEnumerator()`. Например, по причине неинициализированности экземпляра `ImmutableArray<decimal>`, или по какой-то другой причине для экземпляра какой-то другой структуры. Но точно так же алгоритму могут передать не-null ссылку на объект ссылочного типа, который тоже бросит `System.InvalidOperationException` в методе `GetEnumerator()`. Junk in — junk out.
V>Все эти вещи насчет инициализаций структур дефолтными конструкторами надо решать в комплексе
Напомню, что изначально вопрос обобщённых алгоритмов поднимался вне контекста дефолтной инициализации. Просто как утверждение «собаку съел, в поисках классов таких алгоритмов, которые, таки, работают в обоих случаях и в попытках сформулировать ограничения на такие алгоритмы».
Я же утверждаю, что вполне можно придумывать такие обобщённые алгоритмы с констрейнтами, которым безразлично, передают ли им объекты структур или классов. Вот как суммирование в примере выше.
Здравствуйте, Qbit86, Вы писали:
Q>Почему этот алгоритм будет работать для reference-типов и не будет для value-типов, или наоборот?
Ты опять мне пытаешься доказать, что такие алгоритмы, корректно работающие для вэлью и реф типов, всё-таки, есть?
Т.е., пытаешься мне навязать некую удобную для оспаривания позицию?
Не взлетит... ))
Q>Если алгоритму передают некорректный экземпляр value-типа, то алгоритм поведёт себя так же, как если бы ему подали по не-null ссылке некорректный экземпляр reference-типа.
Не туда ты опять смотришь.
Я могу в прикладном виде описать гарантии, которые не дадут мне привести ref-объект в невалидное состояние.
Для value-типов ср-в обеспечения таких гарантий нет.
Однакое, есть сценарии, когда такие гарантии "почти есть".
Например — value-тип являться приватным полем ref-типа и инициализируется в конструкторе.
Помимо этого у обрабатывающих ф-ий стоит модификатор ref в аргументах, что тоже уменьшает вероятность "просто случайно подать".
Помимо этого такие структуры у меня НЕ реализуют известные библиотекам фреймворка интерфейсы, чтобы, опять же, защититься на уровне системы типов от "случайно не туда подали через неявный боксинг".
И т.д. и т.п.
Т.е. я насобирал много правил, помогающих уменьшать вероятность ошибок при проектировании в value-типах, но не смог найти 100% работающей защиты от неправильного использования, т.е. не смог поручить контроль за правильностью использования компилятору. Т.е. только ручками подобную корректность обеспечиваем в любом случае.
Q>Алгоритму могут передать объект значимого типа, который бросит `System.InvalidOperationException` в методе `GetEnumerator()`.
Исключения "not implemented" или "invalid operation" — это первые признаки плохой архитектуры.
Простой пример. Вот есть класс TcpClient.
У него 80% методов выбрасывают "invalid operation".
Почему? Потому что для этого типа оставили пустой конструктор и еще несколько "глупых" конструкторов.
В итоге, объект может находиться в невалидном (неинициализированном) состоянии.
Если убрать пустой конструктор, то у нас пропадает надобность в св-ве Active, а так же уходят все места в его внутреннем коде, которые проверяют это св-во.
В классе TcpClient необходимо было оставить только конструктор от аргументов host+port, а так же добавить конструктор от Socket.
В случае конструирования от Socket можно проверить инвариант сразу, проверив RemoteEndPoint или Connected, т.е. запретить конструировать объект TcpClient в невалидном состоянии.
На самом деле класс TcpClient вообще является убожеством мысли как таковым. Он не даёт над классом Socket никакой другой дополнительной функциональности кроме как одного метода GetStream(). Что мешало добавить GetStream() в сокет — загадка великая есть.
Q>Но точно так же алгоритму могут передать не-null ссылку на объект ссылочного типа, который тоже бросит `System.InvalidOperationException` в методе `GetEnumerator()`.
Могут. Но это будут проблемы того ССЗБ, которому нравится InvalidOperationException.
Мне же нужна возможность конструировать всегда валидные объекты.
V>>Все эти вещи насчет инициализаций структур дефолтными конструкторами надо решать в комплексе Q>Напомню, что изначально вопрос обобщённых алгоритмов поднимался вне контекста дефолтной инициализации.
Не отмажешься, не пытайся. ))
Структуры принципиально отличаются от ref-типов буквально двумя побочными эффектами:
— дефолтной инициализацией;
— передачей копий значений, в т.ч. через неявный боксинг.
В последнем случае происходят свои приколы — например, в некоем алгоритме будет изменяться содержимое копии, а не целевого экземпляра структуры.
Т.е. контекст был один — дотнет.
Тут ведь как? — ну не понял и не понял, хосподя.
Было бы тебе интересно — переспросил бы.
Q>Просто как утверждение «собаку съел, в поисках классов таких алгоритмов, которые, таки, работают в обоих случаях и в попытках сформулировать ограничения на такие алгоритмы».
Ну? Что не так-то?
Ты ведь не единственный тут такой, которые вместо того, чтобы уточнить подробности (если вообще охота дальше продолжать обсуждение) всегда первым делом переходят в несознанку/отрицалово и требуется десятки постов, чтобы разгрести все эти нагромождения "нет, нет и еще раз нет!".
А чего "нет", куда "нет" и почему вообще "нет"-то?... а ХЗ. Так надо! ))
Законы жанра?
Q>Я же утверждаю, что вполне можно придумывать такие обобщённые алгоритмы с констрейнтами, которым безразлично, передают ли им объекты структур или классов. Вот как суммирование в примере выше.
Ну так и я тебе ровно этот случай указал — это когда целевая структура не передаётся сама, рядом с ней надо передавать "словарь операций".
Причем, тут мало того, что таким образом можно хоть как-то задействовать арифметику.
Тут еще чисто вероятностный момент работает — когда надо передать два связанных строгим типовым отношением объекта куда-то, то на порядки сложнее допустить ошибку случайно, в этом месте просыпается внимательность программиста, ЧТД. Потому что есть такая фишка — основная доля ошибок делается в простых местах, в сложных местах ошибок обычно меньше.
Но все-равно, в отуствии ср-в обеспечения гарантий валидности объектов вероятность споткнуться об это остаётся ненулевой, даже для случая оперрования парой типов.
Собсно, моё разочарование в дотнете больше было не от его тормознуточти, а от непоследовательной системы типов, которая больно бъёт по рукам аккурат тогда, когда я пытаюсь с этой тормознутостью бороться, ы-ы-ы! )))
Нифига себе!
"Ах вот вы как? Ну ОК, не больно-то и хотелось..."
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, samius, Вы писали:
S>>За счет чего разница будет в 2.5 раза, если ты уменьшил уровень косвенности лишь у одного вызова на забег по коллекции, в то время как 2*n вызовов осталось ровно с той же косвенностью, как и при передаче интерфейса?
V>В этой таблице результаты джита x86. Очевидно, что этот джит не провел для тестового цикла оптимизацию, т.е. не выкинул из его тела обращение к полям объекта "честным образом", т.е. через переменную, указанную в исходнике теста.
Причем тут вообще эта таблица и джит? Я пытаюсь выяснить у тебя, за счет чего ты ожидаешь уменьшение косвенности в методах типа
int Sum<T>(T coll) where T : IEnumerable<int>
Никакая джит оптимизация цикла в этом конкретном Sum не может отменить тот факт, что цикл будет дергать MoveNext/Current через интерфейс. Так чем же он лучше, чем обычный