Здравствуйте, Serginio1, Вы писали:
S>>>T4 работают, EP>>Причём тут T4? S> А при том, что можно сначала разворачивать исходный код дженериков в код на C# с инлайнингом по типу. Самый простой вариант.
Ты предлагаешь брать на себя работу компилятора и разворачивать через кодогенерацию дженерики во всех комбинациях?
Так об этом и речь — для быстрого кода на C# приходится отказываться от встроенных абстракций и писать вручную низкоуровневый код, либо генерировать его внешними утилитами
Re[27]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, Evgeny.Panasyuk, Вы писали:
S>> А при том, что можно сначала разворачивать исходный код дженериков в код на C# с инлайнингом по типу. Самый простой вариант.
EP>Ты предлагаешь брать на себя работу компилятора и разворачивать через кодогенерацию дженерики во всех комбинациях? EP>Так об этом и речь — для быстрого кода на C# приходится отказываться от встроенных абстракций и писать вручную низкоуровневый код, либо генерировать его внешними утилитами
Я предлагаю заниматься этим компилятору. Ничего сложного. Кроме того куча тулз которые из MSIL кода генерят C# код.
Ни от чего не нужно отказываться. Другое дело, что для лучшей работы как я тебе показывал можно ввести расширения имплементации интерфейсов для типов.
При этом можно в том числе реализовывать и статический SQL из Linq при привязке провайдера.
и солнце б утром не вставало, когда бы не было меня
Здравствуйте, ·, Вы писали: EP>>·>Тут не в сомнениях дело. А сам факт того, что если условие ассерта не сработает в релизе — то угадать что будет — невозможно. EP>>assert'ы применяются намного шире чем простейший bounds checking — для проверки пред/пост-условий, для проверки инвариантов и вариантов циклов, для проверки инвариантов классов и т.п. ·>А что они дают-то? Почему вместо assert x просто нельзя if(!x) throw Boom()?
Почему нельзя? — Можно (есть конечно случаи где нужен именно no-throw код, иначе в процессе раскрутки поломаются другие инварианты, но это отдельная тема, не особо влияющая на суть данной дискуссии).
Отличительная черта assert'ов — в том что они могут быть опционально отключены, более того — для формально верифицированного кода они не нужны. ·>Если медленно — это уже проблема алгоритма.
Это ещё почему? ·>При хорошем покрытии тестами ценность ассертов падает до нуля.
Точно также как и ценность других defensive примочек, а-ля bounds checking EP>>С другой стороны, выключение части из проверок в runtime'е, ровно как и отсутствии часть проверок вообще — не делает программу некорректной. ·>Но только если ассерты расставлены корректно.
Что ты имеешь в виду? То что проверка внутри assert'а может иметь side-effect? EP>>>>ЕМНИП Sinix рассказывал что использует их во всю как раз в managed языке. EP>>·>Можно, конечно. Но непонятно зачем. А где он об этом писал? EP>>Много где, можем попробовать его призвать сюда. Например в CodeJam сделали компонент Assertions. ·>Я что-то не понял что это. Судя по if (!condition)throw это не ассерты в понимании С++.
Там некоторые из них опциональные, и не попадают в Release. EP>>В целом не пойму почему ты проводишь разделение managed/не managed относительно assert'ов. ·>Это в ответ на то, что в С++ "включаются asserts, checked iterators и прочий defensive programming". Ассерты тут как частный случай скорее. ·>Т.е. в managed у тебя всегда работает правильно, а когда может — работает быстро.
Ты как-то перекручиваешь слова. Не "правильно", а раннее обнаружение бага, что вовсе не означает "правильно" — твой код уже пришёл в состояние в котором он быть не должен был, и неизвестно как он туда попал, и что уже натворил по пути. ·>В unmanaged наоборот: когда может — работает правильно, и всегда быстро.
Не всегда быстро, только когда assert'ы отключены. EP>>·>Они позволяют избежать UB. Грубо говоря некий такой wysiwyg — как код написан, так он и исполняется. А не так, что вышли за границы массива или обратились к неинициализированной памяти — и последствия совершенно непредсказуемы. EP>>Это лишь только для мизерной части утверждений корректности программы. Если ты не расписал все утверждения корректности, то никакого "WYSIWYG" — очевидно нет ·>Есть конечно. Я говорю о корректности самого кода, а не логических ошибок в алгоритме.
В чём ты видишь отличие? Выход за пределы bounds это есть логическая ошибка, которой в корректном коде нет ·>Т.е. в С++ если видишь, что у тебя в коде написано a = b + c то при известных значениях b==1 и c==2 может случиться, что a!=3 т.к. где-то что-то стрельнуло по памяти из другого треда. В managed — a==3 всегда, доказано верификатором.
Не доказано — ибо точно также используется не-managed код, точно также есть компилятор в котором могут быть баги, и даже железо может сбоить EP>>P.S. Ничто не мешает оставить часть assert'ов включенных в Release. Для этого их обычно разбивают на классы "быстрые", "медленные", "очень медленные" — и уже пользователь библиотеки решает что попадёт в Release. ·>По-моему излишнее усложнение. Собственно на основании чего пользователь будет принимать решение?
1. На основании того какое быстродействие его устраивает
2. На основании того насколько корректен его код (здесь речь идёт про библиотеку и пользовательский код, ассерты внутри библиотеки могут ловить ошибки в пользовательском коде).
Здравствуйте, Serginio1, Вы писали:
S>>>>>T4 работают, EP>>>>Причём тут T4? S>>> А при том, что можно сначала разворачивать исходный код дженериков в код на C# с инлайнингом по типу. Самый простой вариант. EP>>Ты предлагаешь брать на себя работу компилятора и разворачивать через кодогенерацию дженерики во всех комбинациях? EP>>Так об этом и речь — для быстрого кода на C# приходится отказываться от встроенных абстракций и писать вручную низкоуровневый код, либо генерировать его внешними утилитами S> Я предлагаю заниматься этим компилятору. Ничего сложного.
И причём тут тогда T4?!
Re[29]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
EP>>>Так об этом и речь — для быстрого кода на C# приходится отказываться от встроенных абстракций и писать вручную низкоуровневый код, либо генерировать его внешними утилитами S>> Я предлагаю заниматься этим компилятору. Ничего сложного.
EP>И причём тут тогда T4?!
Я просто привел аналог раскрутки шаблонов. Можно вспомнить про макросы Немерле. Да и в C++ макросы оочень часто используются https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BE%D1%80_%D0%A1%D0%B8
и солнце б утром не вставало, когда бы не было меня
Здравствуйте, Serginio1, Вы писали:
EP>>>>Так об этом и речь — для быстрого кода на C# приходится отказываться от встроенных абстракций и писать вручную низкоуровневый код, либо генерировать его внешними утилитами S>>> Я предлагаю заниматься этим компилятору. Ничего сложного. EP>>И причём тут тогда T4?! S> Я просто привел аналог раскрутки шаблонов.
Зачем? Никто же не спорит с тем что можно делать вручную то что делают компиляторы C++
S>Можно вспомнить про макросы Немерле.
T4 очень далеко до макросов Nemerle.
S>Да и в C++ макросы оочень часто используются
Во-первых не часто.
Во-вторых изначально Страуструп пытался использовать макросы для обобщённого программирования — это очень неудобно, собственно поэтому мы и имеем шаблоны. Ты же предлагаешь воспроизводить грабли тридцатилетней давности посредством текстовых макросов
Re[25]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, Serginio1, Вы писали:
EP>>>То есть ещё раз, код C# чисто по построению намного труднее оптимизировать — для оптимизаций до уровней аналогичных C++ нужно либо язык модифицировать, либо делать оптимизаторы намного более мощные чем оптимизаторы C++ S>> А можно поподробнее чем S>>
Эмм. Это будет иметь значение только в том случае, если мы рассматриваем инлайнинг f. А надо рассматривать инлайнинг foo — потому что после него код приобретает вид
class ConcreteF
{
int x;
int callee() { return x;}
}
var cf = new ConcreteF(0);
return cf.callee();
Это возможно на обоих языках, т.к. foo у нас невиртуальная и маленького размера. После этого нам доступны дополнительные оптимизации:
1. Девиртуализация вызова cf.callee() делается на раз-два, мы же знаем точный тип cf независимо от того, как он объявлен. Это частный случай constant propagation.
2. После девиртуализации мы инлайним callee(), но не внутри foo, что сложно сделать (и в C++ это возможно исключительно благодаря поставке foo в исходниках. Стоит перенести определение foo из .h в .cpp, как он окажется на равных с С#), а внутри main, где нам всё известно.
3. повторяем constant propagation и получаем int main() { return 0; }.
То, что это не делается в C#/CIL/JIT — не ограничение языка C#. Это ограничение JIT/ngen. Нет принципиальных причин так не делать.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[26]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, Sinclair, Вы писали:
S>Эмм. Это будет иметь значение только в том случае, если мы рассматриваем инлайнинг f. А надо рассматривать инлайнинг foo — потому что после него код приобретает вид S>
S>class ConcreteF
S>{
S> int x;
S> int callee() { return x;}
S>}
S>var cf = new ConcreteF(0);
S>return cf.callee();
S>
Не верно, так как непосредственно после инлайнинга получаем:
class ConcreteF
{
int x;
int callee() { return x;}
}
Func<...> cf = new ConcreteF(0);
return cf.callee();
И это простейший случай. В более сложных вариантах замыкания могут не просто передаваться через параметры, а встраиваться в другие объекты, либо другие замыкания — например композиция двух замыкания.
S>Это возможно на обоих языках, т.к. foo у нас невиртуальная и маленького размера. После этого нам доступны дополнительные оптимизации: S>1. Девиртуализация вызова cf.callee() делается на раз-два, мы же знаем точный тип cf независимо от того, как он объявлен. Это частный случай constant propagation.
Так вот я и говорю, что даже C++ компиляторы делают подобную оптимизацию только для простых случаев
. И это причём GCC, а не отстающий MS.
Такая оптимизация сложнее простого инлайнинга невирутальных функций, о чём собственно я и говорю, и требует больше уровней инлайнинга.
S>(и в C++ это возможно исключительно благодаря поставке foo в исходниках. Стоит перенести определение foo из .h в .cpp, как он окажется на равных с С#)
1. Для этого есть Link-Time Code Generation / Link Time Optimization
2. Замыкания, ФВП и прочее в большинстве случаев именно в исходниках и поставляются.
S>То, что это не делается в C#/CIL/JIT — не ограничение языка C#.
Именно из ограничений языка и вытекают дополнительные сложности оптимизации.
Re[29]: {@Sinix} Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>>>assert'ы применяются намного шире чем простейший bounds checking — для проверки пред/пост-условий, для проверки инвариантов и вариантов циклов, для проверки инвариантов классов и т.п. EP>·>А что они дают-то? Почему вместо assert x просто нельзя if(!x) throw Boom()? EP>Почему нельзя? — Можно (есть конечно случаи где нужен именно no-throw код, иначе в процессе раскрутки поломаются другие инварианты, но это отдельная тема, не особо влияющая на суть данной дискуссии). EP>Отличительная черта assert'ов — в том что они могут быть опционально отключены,
Вот эта опциональность и не нравится. Добавляет ещё одну степень свободы в код, т.е. лишнюю сложность. Либо уж пусть будет тестовый код, прогоняемый во время билда, но не попадающий в релиз, либо релизный код, который работает всегда.
EP>более того — для формально верифицированного кода они не нужны.
Для формально верифицированного их разве кто-то пишет?
EP>·>Если медленно — это уже проблема алгоритма. EP>Это ещё почему?
Точнее не так. Если медленно, то можно перенести ассерт в юнит-тест.
Добавление ассертов можно оправдать на большой старой кодобазе без единого ЮТ. Ассерт добавить обычно на порядки проще чем ЮТ. Ручные тестеры будут тестить дебаждую сборку. Или давать клиенту и попросить зарепродюсить прод-баг.
EP>·>При хорошем покрытии тестами ценность ассертов падает до нуля. EP>Точно также как и ценность других defensive примочек, а-ля bounds checking
Не точно также же. В проде bounds checking валит исключения, а ассерты — ничего не делают.
EP>>>С другой стороны, выключение части из проверок в runtime'е, ровно как и отсутствии часть проверок вообще — не делает программу некорректной. EP>·>Но только если ассерты расставлены корректно. EP>Что ты имеешь в виду? То что проверка внутри assert'а может иметь side-effect?
Да, и такое бывает, к сожалению.
EP>·>Я что-то не понял что это. Судя по if (!condition)throw это не ассерты в понимании С++. EP>Там некоторые из них опциональные, и не попадают в Release.
Ух ты. Это как устроено? Что-то не могу разобраться...
EP>>>В целом не пойму почему ты проводишь разделение managed/не managed относительно assert'ов. EP>·>Это в ответ на то, что в С++ "включаются asserts, checked iterators и прочий defensive programming". Ассерты тут как частный случай скорее. EP>·>Т.е. в managed у тебя всегда работает правильно, а когда может — работает быстро. EP>Ты как-то перекручиваешь слова. Не "правильно", а ранее обнаружение бага, что вовсе не означает "правильно" — твой код уже пришёл в состояние в котором он быть не должен был, и неизвестно как он туда попал, и что уже натворил по пути.
"мой код", а не языковая конструкция.
EP>·>В unmanaged наоборот: когда может — работает правильно, и всегда быстро. EP>Не всегда быстро, только когда assert'ы отключены.
При релизном использовании отключены.
EP>·>Есть конечно. Я говорю о корректности самого кода, а не логических ошибок в алгоритме. EP>В чём ты видишь отличие? Выход за пределы bounds это есть логическая ошибка, которой в корректном коде нет
Эта ошибка явно прописана в спеке языка — результат — ArrayOutOfBoundException. Т.е. работает по спеке, однозначно, а не когда вместо исключения у тебя случится, что 1+2=42.
EP>·>Т.е. в С++ если видишь, что у тебя в коде написано a = b + c то при известных значениях b==1 и c==2 может случиться, что a!=3 т.к. где-то что-то стрельнуло по памяти из другого треда. В managed — a==3 всегда, доказано верификатором. EP>Не доказано — ибо точно также используется не-managed код, точно также есть компилятор в котором могут быть баги, и даже железо может сбоить
Это понятно. Но для типичного приложения компилятор и железо таки можно считать надёжными. К ним применяют гораздо более серьёзные подходы, типа формальной верификации, да и квалификация инженеров выше как правило.
EP>>>P.S. Ничто не мешает оставить часть assert'ов включенных в Release. Для этого их обычно разбивают на классы "быстрые", "медленные", "очень медленные" — и уже пользователь библиотеки решает что попадёт в Release. EP>·>По-моему излишнее усложнение. Собственно на основании чего пользователь будет принимать решение? EP>1. На основании того какое быстродействие его устраивает
Чем быстрее, тем лучше, очевидно.
EP>2. На основании того насколько корректен его код (здесь речь идёт про библиотеку и пользовательский код, ассерты внутри библиотеки могут ловить ошибки в пользовательском коде).
Конечно, корректен, я что ламер некорректный код писать? А ты как корректность своего кода оцениваешь?
А вообще для этого сценария придумали логи и кастомизируемые (в т.ч. в рантайм) уровни логгирования. Ассерты тут пихать не надо.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[31]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, Evgeny.Panasyuk, Вы писали:
S>>А чего там обсуждать-то? EP>·>Можно, конечно. [bНо непонятно зачем[/b].
А. Ну, у меня отпуск, настроение хорошее, спорить не тянет Незачем, значит незачем.
EP>Да, это преимущество. Но с другой стороны в юнит тестами намного проще перевести код в состояние всех edge-case'ов (в которых в том числе могут выстрелить assert'ы) по сравнению с интеграционными тестами.
Ага. Но это важно в первую очередь для всяких хелперов, т.е. кусков кода, которые будут переиспользоваться в самых неожиданных местах. Вот там юнит-тесты и проверки всяких нежданчиков однозначно нужны.
А в сложных штуках, которые из этих самых хелперов собираются, оказывается полно кода, который хоть и простой, но всё равно хорошо бы проверить. Но при этом не разбирать код на доступные тестам куски, т.к. это заметно дороже обходится. Комбинации из ассертов и интеграционных тестов тут альтернативы нет, кмк.
EP>100% code coverage не даёт гарантии попадания во все edge cases.
Ага. Для меня code coverage — удобнейший инструмент для подсветки новых / изменённых кусков кода, которые ещё не покрыты тестами. Само качество покрытия, понятное дело, автоматом не проверяется никак (если говорить про реальные языки, а не про фантастику с полной формальной верификацией кода), только ручками.
Re[30]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, ·, Вы писали:
EP>>более того — для формально верифицированного кода они не нужны. ·>Для формально верифицированного их разве кто-то пишет?
Я имею в виду то, что после того как есть результат формальной верификации, можно их выключить и это никак не повлияют на корректность.
EP>>·>Если медленно — это уже проблема алгоритма. EP>>Это ещё почему? ·>Точнее не так. Если медленно, то можно перенести ассерт в юнит-тест.
Sinix выше правильно заметил — assert'ы отрабатывают во время всех тестов, как юнит, так и интеграционных, и поэтому есть приличный шанс того что они поймают баг пропущенный непосредственно юнит-тестированием.
Более того, юнит-тест конкретного компонента может успешно пройти при наличии бага, или например двойного бага — то есть false positive. При этом внутренний assert мог поймать эти баги на этом же прогоне. То есть assert'ы это отличное дополнение unit-тестов.
EP>>·>При хорошем покрытии тестами ценность ассертов падает до нуля. EP>>Точно также как и ценность других defensive примочек, а-ля bounds checking ·>Не точно также же. В проде bounds checking валит исключения
Да какая разница если у тебя в оттестированном и корректном коде не будет этих исключений?
То есть ты определись — либо у тебя код оттестирован и defensive programming не нужен, либо не оттестирован и ты тестируешь его на своих пользователях
EP>>>>С другой стороны, выключение части из проверок в runtime'е, ровно как и отсутствии часть проверок вообще — не делает программу некорректной. EP>>·>Но только если ассерты расставлены корректно. EP>>Что ты имеешь в виду? То что проверка внутри assert'а может иметь side-effect? ·>Да, и такое бывает, к сожалению.
Я уже сказал что это ловится тестированием и debug и release версий. Но встречается крайне редко — я видел подобное только когда человек не знал что такое assert.
EP>>·>Я что-то не понял что это. Судя по if (!condition)throw это не ассерты в понимании С++. EP>>Там некоторые из них опциональные, и не попадают в Release. ·>Ух ты. Это как устроено? Что-то не могу разобраться...
Conditional methods allow developers to create methods whose calls can be placed in the code and then either included or omitted during compilation based on a preprocessing symbol.
EP>>·>В unmanaged наоборот: когда может — работает правильно, и всегда быстро. EP>>Не всегда быстро, только когда assert'ы отключены. ·>При релизном использовании отключены.
Совсем необязательно — я их видел в коммерческом продукте имеющем сотни миллионов установок
·>>>Есть конечно. Я говорю о корректности самого кода, а не логических ошибок в алгоритме. EP>>В чём ты видишь отличие? Выход за пределы bounds это есть логическая ошибка, которой в корректном коде нет ·>Эта ошибка явно прописана в спеке языка
Да какая разница — это и есть логическая ошибка в твоём алгоритме.
EP>>>>P.S. Ничто не мешает оставить часть assert'ов включенных в Release. Для этого их обычно разбивают на классы "быстрые", "медленные", "очень медленные" — и уже пользователь библиотеки решает что попадёт в Release. EP>>·>По-моему излишнее усложнение. Собственно на основании чего пользователь будет принимать решение? EP>>1. На основании того какое быстродействие его устраивает ·>Чем быстрее, тем лучше, очевидно.
Совсем не очевидно, ибо это не единственный критерий.
EP>>2. На основании того насколько корректен его код (здесь речь идёт про библиотеку и пользовательский код, ассерты внутри библиотеки могут ловить ошибки в пользовательском коде). ·>Конечно, корректен, я что ламер некорректный код писать?
Тогда тебе и не нужен никакой defensive programming
·>А ты как корректность своего кода оцениваешь?
Зависит от конкретного случая — различные комбинации unit/integration тестов, разбиение на классы эквивалентности, поиск изоморфизмов, assert'ы на пред/пост-условия и ванрианты/инварианты, доказательства. Вот до чего пока не доходил, так это до выражения доказательств на формальных языках и их дальнейшей автоматизированной проверки.
·>А вообще для этого сценария придумали логи и кастомизируемые (в т.ч. в рантайм) уровни логгирования. Ассерты тут пихать не надо.
А причём тут логи? Если у тебя условие assert'а сфейлилось — то дальше всё может полететь в тартарары, и лучше всего пристрелить такую программу.
Здравствуйте, gandjustas, Вы писали:
EP>>>>То есть ещё раз, код C# чисто по построению намного труднее оптимизировать — для оптимизаций до уровней аналогичных C++ нужно либо язык модифицировать, либо делать оптимизаторы намного более мощные чем оптимизаторы C++ G>>>Язык тут не при чем. EP>>Именно язык тут и при чём. Если писать на C++ в стиле C# — динамические замыкания, динамический IEnumerable, множество индерекций по памяти — то получим примерно такие же тормоза G>Что такое "данимические замыкания" ? В IL замыкания превращаются просто в классы.
При этом передаются через параметры, сохраняются в классы и т.п. посредством динамического полиморфизма, стирая исходный тип.
G>Что такое "динамический IEnumerable"?
Динамический полиморфизм для обхода последовательностей.
G>Это в смысле IEnumerable не по массивам? Зачем его использовать в performance-critical коде?
Затем что последовательности могут быть выражены разными структурами данных.
Собственно о том и речь — в C++ я могу накручивать уровни абстракций даже в performance-critical коде, без жёсткого penalty.
G>А IEnumerable (foreach) с массивами действительно тормозит (накладные расходы на Enumerator оказываются больше, чем затратры на тело цикла). Как раз из-за недостатка инлайнинга, в C++ foreach нилайнится.
Так дело не в самом foreach — ты если распишешь его вручную, то он также будет тормозить, за счёт динамического полиморфизма..
G>Или ты имеешь ввиду использование ФВП для обхода массивов? Оно действительно и в C++, и в C# плохо работает. И в C++ и в C# такой код вручную оптимизируется.
ФВП в том числе. А с чего ты взял что они в C++ плохо работают? Наоборот, прекрасно работают и отлично оптимизируются.
Пример C++ -> JS vs C# как раз ФВП и использует.
G>>>Машинный код генерируется из IL. В нем все указанные тобой оптимизации делаются элементарно. G>>>Но у JIT нет столько времени на пребразования, как у компилятора C++. EP>>Что мешает сразу генерировать оптимизированный IL? G>IL — высокоуровневый язык. Он и так достаточно оптимален на своем уровне.
И что из этого? C# версия без ФВП тоже компилируется в тот же самый "высокоуровневый IL", но работает быстрее. Версия с ФВП могла бы давать точно такой же IL.
EP>>Я выше привёл пример с трансляцией C++ -> JS. JS ещё "хуже" IL, но тем не менее он работает быстро. G>Что "хуже" ?
Тем что JS более высокоуровневый.
G>Работает быстро за счет "hotspot". Большинство движков JS перекомпилируют код на основании профиля использования. Кроме того "работает быстро" — это когда не используются ФВП.
Вот именно, в исходном коде на C++ были ФВП и замыкание, в результирующем JS они были выоптимизированы компилятором C++, так как это элементарная операция (никакого динамического полиморфизма) — именно поэтому и быстро.
EP>>Можно взять этот JS выхлоп и перевести на C# — и он там тоже будет работать быстро, несмотря ни на какой IL G>Если не используются ФВП, то да.
ФВП в коде на C++ есть, но в выхлопе оптимизатора их нет.
G>Вообще при небольших усилиях скорость C# аналогична C++. Даже статью на эту тему писал https://habrahabr.ru/post/266373/
Твой тест низкоуровневый, без абстракций, и полностью подтверждает мой тезис, точнее вторую часть:
C++ быстрый не от того что нативный, а от того что даёт бесплатные, либо крайне дешёвые абстракции. Если на C#/Java писать код без абстракций, то по скорости он будет близок к аналогу на C++ — но такого кода будет намного больше
А вот первую часть ты можешь проверить сделав сортировку похожей на реальную, а именно абстрагировавшись от конкретного контейнера, конкретного типа элемента, и конкретного предиката. Например сравнив:
template<typename RandomAccessIterator, typename BinaryPredicate>
void sort(RandomAccessIterator m, unsigned n, BinaryPredicate p)
vs
void Sort(IList<T> m, Comparison<T> comparison)
Re[29]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
EP>И всё же ты не понимаешь. Класс да, генерируется, да размер на стороне вызова известен, но вот внутри дженерика размер стёрт, даже после всех подстановок, в C++ же его размер внутри шаблона известен во время компиляции.
Какой размер внутри дженерика? Кто стер? В .NET вся информация о типах хранится, поэтому размер известен не только в compile-time, но и в run-time.
EP>Перечитай десять раз, посмотри примеры выше, обрати внимание на static_assert — если не дойдёт, то задавай конкретный вопрос.
Ты приведи пример на C#, который иллюстрирует то, что ты говоришь. Потому что сейчас это звучит как бред сумасшедшего.
EP>>>В C# же у тебя будет Func<...> одинакового размера для всех вариантов замыканий, независимо от их оригинального размера.. G>>Ты наверное что-то другое хочешь сказать, но не можешь это словами выразить. EP>Я уже выразил, и пример привёл. Но ты, из-за своего неконструктивного настроя, не можешь это понять.
Ты выразил что-то про C#, а пример привел на C++. Это разные языки если что. Ты на C# пример приведи.
EP>>>2. Речь даже не столько про стоимость аллокации, а про то что она сильно мешает последующему инлайнингу. G>>Ты причину и следствие путаешь. EP>Не путаю.
Путаешь.
G>>Убрать аллокацию для замыкания можно, если вызов делегата можно заинлайнить по месту вызова. Но это не значит что наличие аллокации мешает инлайну. EP>Именно мешает. Я смотрел генерируемый код C++ компиляторами — простые виртуальные вызовы инлайнятся, если же вставить динамическую аллокацию (при прочих равных), то инлайна не происходит.
Так то ограничение компилятора C++, принципиальных проблем инлайна с аллокациями нет. JIT умеет инлайнить методы с new.
G>>>>Даже с учетом сборки мусора амортизированная стоимость аллокации невелика. EP>>>Она огромна по сравнению с отсуствием аллокаций G>>В реальной программе аллокаций будет дофига в любом случае.
EP>Это на C# дофига аллокаций, на C++ их на порядки меньше, так как многое хранится по значению. Вектор объектов класса — это минимум одна аллокация, на C# — минимум N+1 аллокаций. EP>И в любом случае 100*дофига намного больше чем просто дофига
Разница будет даже не в разы, а в единицы процентов. А вот количество аллокаций будет для .NET выше.
EP>>>>>Виртуальный вызов и прочее можно соптимизировать, но это очевидно более сложная задача чем заинлайнить простой вызов конкретного метода G>>>>JIT занимается девиртуализацией вызовов, причем успешно. EP>>>Ты читать не умеешь? "это очевидно более сложная задача" G>>С чего ты взял что она сложная? Почти примитивная оптимизация. EP>И всё таки ты читать не умеешь "более сложная"
Более сложная чем что? Какую еще часть твоей фразы надо прочитать, чтобы понять тайный смысл того, что ты пишешь?
EP>>>В любом случае это будет медленная динамическая хрень, против обычного вызова невиртуальной функции. И эту хрень намного тяжелее оптимизировать, простым преобразования C# -> C++ не отделаешься G>>Это надо проверить. EP>Что конкретно?
Мои тесты показывают, что .NET Native после компиляции дает такую же скорость, как аналогичный рукопашный код на C++.
И "медленная динамическая хрень" на .NET вполне нормально работает при прямой компиляции в C++.
Re[27]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, Evgeny.Panasyuk, Вы писали:
G>>Что такое "данимические замыкания" ? В IL замыкания превращаются просто в классы. EP>При этом передаются через параметры, сохраняются в классы и т.п. посредством динамического полиморфизма, стирая исходный тип.
Ты бредишь
1) В замыканиях нетполиморфизма
2) В замыканиях нет женериков
3) В .NET вообще нет стирания типов в женериках
G>>Что такое "динамический IEnumerable"? EP>Динамический полиморфизм для обхода последовательностей.
Пример кода покажи, не ясно о чем ты пишешь.
G>>Это в смысле IEnumerable не по массивам? Зачем его использовать в performance-critical коде? EP>Затем что последовательности могут быть выражены разными структурами данных.
Не пойму о чем ты. Если у тебя массив или строка — пиши обычный цикл. Если там не массив и не строка, то пофиг — IEnumerable нормально работает.
EP>Собственно о том и речь — в C++ я могу накручивать уровни абстракций даже в performance-critical коде, без жёсткого penalty.
Это только если инлайнинг сработает. Об этом тебе и писали, дело не в языке, а в компиляторе. К сожалению для .NET негде такую оптимизацию производить.
G>>А IEnumerable (foreach) с массивами действительно тормозит (накладные расходы на Enumerator оказываются больше, чем затратры на тело цикла). Как раз из-за недостатка инлайнинга, в C++ foreach нилайнится. EP>Так дело не в самом foreach — ты если распишешь его вручную, то он также будет тормозить, за счёт динамического полиморфизма..
Для обычных массивов jit оптимизирует foreach
var xs = new[] { 1, 2, 3 };
var s = 0;
foreach (var item in xs)
{
s += item;
}
выхлоп:
var s = 0;
019A34D5 xor esi,esi
foreach (var item in xs)
019A34D7 xor ecx,ecx
019A34D9 mov edi,dword ptr [edx+4]
019A34DC test edi,edi
019A34DE jle 019A34EB
foreach (var item in xs)
019A34E0 mov eax,dword ptr [edx+ecx*4+8]
{
s += item;
019A34E4 add esi,eax
019A34E6 inc ecx
foreach (var item in xs)
019A34E7 cmp edi,ecx
019A34E9 jg 019A34E0
}
Найди тут виртуальные вызовы.
Для не-массивов применяется оптимизация:
1) Реализация IEnumerator делается структурой
2) GetEnumerator возвращает структуру
3) foreach в этом случае не использует ни аллокаций, ни приведений к интерфейсу, ни полиморфизм
C IEnumerable другая проблема, которая связана с ФВП. Вообще ФП в C# довольно медленно работает, не создавался язык под него.
G>>Или ты имеешь ввиду использование ФВП для обхода массивов? Оно действительно и в C++, и в C# плохо работает. И в C++ и в C# такой код вручную оптимизируется.
EP>ФВП в том числе. А с чего ты взял что они в C++ плохо работают? Наоборот, прекрасно работают и отлично оптимизируются. EP>Пример C++ -> JS vs C# как раз ФВП и использует.
За счет инлайнинга, который JIT не делает.
Но ты же не будешь в performance-critical на автоматический инлайнинг полагаться.
G>>>>Машинный код генерируется из IL. В нем все указанные тобой оптимизации делаются элементарно. G>>>>Но у JIT нет столько времени на пребразования, как у компилятора C++. EP>>>Что мешает сразу генерировать оптимизированный IL? G>>IL — высокоуровневый язык. Он и так достаточно оптимален на своем уровне.
EP>И что из этого? C# версия без ФВП тоже компилируется в тот же самый "высокоуровневый IL", но работает быстрее. Версия с ФВП могла бы давать точно такой же IL.
Не могла бы, потому что делегаты. В F# отказались от делегатов, там лучше работает.
EP>>>Я выше привёл пример с трансляцией C++ -> JS. JS ещё "хуже" IL, но тем не менее он работает быстро. G>>Что "хуже" ? EP>Тем что JS более высокоуровневый.
И что?
G>>Работает быстро за счет "hotspot". Большинство движков JS перекомпилируют код на основании профиля использования. Кроме того "работает быстро" — это когда не используются ФВП. EP>Вот именно, в исходном коде на C++ были ФВП и замыкание, в результирующем JS они были выоптимизированы компилятором C++, так как это элементарная операция (никакого динамического полиморфизма) — именно поэтому и быстро.
Ну так ты продолжаешь доказывать, что проблема в языке C#. А проблема в компиляторе, который ФВП не инлайнит. О чем я тебе и говорил с самого начала.
Re[30]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, gandjustas, Вы писали:
EP>>И всё же ты не понимаешь. Класс да, генерируется, да размер на стороне вызова известен, но вот внутри дженерика размер стёрт, даже после всех подстановок, в C++ же его размер внутри шаблона известен во время компиляции. G>Какой размер внутри дженерика?
Размер структуры/класса.
G>Кто стер?
Программист, в том момент когда передал замыкание в функцию принимающую Func<...>. Для разных замыканий тип один и тот же — Func<...>
G>В .NET вся информация о типах хранится, поэтому размер известен не только в compile-time, но и в run-time.
Внутри дженерика эта информация во время компиляции неизвестна. Известно только то что есть в типах, а тип один для разных замыканий Func<...>
EP>>Перечитай десять раз, посмотри примеры выше, обрати внимание на static_assert — если не дойдёт, то задавай конкретный вопрос. G>Ты приведи пример на C#, который иллюстрирует то, что ты говоришь.
Я приведу на C++, а ты попробуй привести аналог на C# (хотя бы псевдокод):
template<unsigned SIZE> void size_is();
template<typename F>
auto foo(F f)
{
size_is<sizeof(F)>();
return f();
}
int main()
{
auto x = 0, y = 1;
foo([=]{ return x; });
foo([=]{ return x + y; });
}
/tmp/cclb4DzB.o: In function `_Z3fooIZ4mainEUlvE_EDaT_':
main.cpp:(.text+0x1c): undefined reference to `void size_is<4u>()'
/tmp/cclb4DzB.o: In function `_Z3fooIZ4mainEUlvE0_EDaT_':
main.cpp:(.text+0x54): undefined reference to `void size_is<8u>()'
collect2: error: ld returned 1 exit status
В foo предаются два разных замыкания с одинаковой сигнатурой. Размеры замыканий разные. Вызов size_is внутри foo демонстрирует то, что внутри foo размеры этих замыканий известны по построению, по правилам языка, даже без работы всяких оптимизаторов.
Вызов f() внутри — полностью статический и прямой, без всяких косвеностей и динамического полиморфизма — опять таки, по построению.
G>Потому что сейчас это звучит как бред сумасшедшего.
Не хами. Если ты чего-то не понимаешь, то необязательно называть это бредом, это не конструктивно. Я всегда привожу необходимые примеры.
EP>>Именно мешает. Я смотрел генерируемый код C++ компиляторами — простые виртуальные вызовы инлайнятся, если же вставить динамическую аллокацию (при прочих равных), то инлайна не происходит. G>Так то ограничение компилятора C++
Так ты же говоришь что код C#/.NET будет транслироваться в C++ — я показываю что только этого недостаточно.
G>принципиальных проблем инлайна с аллокациями нет.
Принципиальных нет, я говорю о том что это сложнее.
Точно также как нет и принципиальных проблем генерировать супер-оптимальный код из Python.
EP>>>>>>Виртуальный вызов и прочее можно соптимизировать, но это очевидно более сложная задача чем заинлайнить простой вызов конкретного метода G>>>>>JIT занимается девиртуализацией вызовов, причем успешно. EP>>>>Ты читать не умеешь? "это очевидно более сложная задача" G>>>С чего ты взял что она сложная? Почти примитивная оптимизация. EP>>И всё таки ты читать не умеешь "более сложная" G>Более сложная чем что? Какую еще часть твоей фразы надо прочитать, чтобы понять тайный смысл того, что ты пишешь?
Выделил. Могу продублировать:
EP>>>>>>Виртуальный вызов и прочее можно соптимизировать, но это очевидно более сложная задача чем заинлайнить простой вызов конкретного метода
EP>>>>В любом случае это будет медленная динамическая хрень, против обычного вызова невиртуальной функции. И эту хрень намного тяжелее оптимизировать, простым преобразования C# -> C++ не отделаешься G>>>Это надо проверить. EP>>Что конкретно? G>Мои тесты показывают, что .NET Native после компиляции дает такую же скорость, как аналогичный рукопашный код на C++.
Твои тесты низкоуровневые. Собственно я выше уже много раз сказал, что на низком уровне на C#/Java можно достичь скорости близкой к C++ (не беря в расчёт автовекторизацию и подобное). Мне не нужно это доказывать, я сам это говорю, андерстенд?
Если же повысить уровень абстракции — сделать сортировку произвольных элементов, произвольных последовательностей, произвольным предикатом — так как выглядят реальные сортировки а-ля std::sort — то результат C++ не изменится, а вот C# во много раз просядет. Собственно мой тест с JS ровно это и продемонстрировал, и там даже абстракций было меньше чем у сортировки.
Re[26]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>И это причём GCC, а не отстающий MS.
Про МС хз, давно на него не смотрел.
Но от качества кода, генерируемого GCC, лично я не в восторге.
Не так давно разбирался почему вдруг одна функция стала жрать дофига стека. В кернеле это критично, так что у нас в build есть всякие анализаторы которые как раз на это и сработали.
Оказалось что GCC для каждого error out return зачем то заранее зарезервировал отдельный кусок стека, и каждый раз сохранял туда все регистры, вызывал какую то мелкую функцию, которая использовала только 2 регистра, вытягивал все регистры назад, сразу же большинство из них занулял и делал ret.
И так много раз, с отдельным куском стека на каждый return. x64 release build.
Такого шлака я нигде ещё не видел.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Забанили по IP, значит пора закрыть эту страницу.
Всем пока
Re[27]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, gandjustas, Вы писали:
CC>>Пару лет назад я показывал как легко и быстро сделать аллокацию в С++ быстрее .NET. G>Видел десяток проектов на C++ — везде стандартный аллокатор (внезапно).
Ну дык любую подсистему начинают трогать только когда она становится узким местом, по результатам профайлинга.
В С++ такое случается довольно редко, потому как там обычно не делают alloc/free на каждый чих.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Забанили по IP, значит пора закрыть эту страницу.
Всем пока
Re[28]: Да ну и фиг с этой Java-ой. .Net будет убит Rust-ом
Здравствуйте, gandjustas, Вы писали:
G>>>Что такое "данимические замыкания" ? В IL замыкания превращаются просто в классы. EP>>При этом передаются через параметры, сохраняются в классы и т.п. посредством динамического полиморфизма, стирая исходный тип. G>Ты бредишь
Хамство это конечно отличный, а главное конструктивный аргумент. Зачем думать, анализировать, спрашивать — когда можно просто нахамить — "не понимаю значит бред"
G>В замыканиях нетполиморфизма
Не в самих замыканиях. Напиши пример ФВП которая принимает простейшее замыкание.
G>>>Что такое "динамический IEnumerable"? EP>>Динамический полиморфизм для обхода последовательностей. G>Пример кода покажи, не ясно о чем ты пишешь.
Вот.
G>>>Это в смысле IEnumerable не по массивам? Зачем его использовать в performance-critical коде? EP>>Затем что последовательности могут быть выражены разными структурами данных. G>Не пойму о чем ты.
std::vector, std::deque, std::string — это всё random access последовательности. На них можно писать алгоритмы на итераторах, без abstraction penalty.
На C# IList даст приличную просадку. И это лишь один из примеров.
EP>>Собственно о том и речь — в C++ я могу накручивать уровни абстракций даже в performance-critical коде, без жёсткого penalty. G>Это только если инлайнинг сработает.
И что характерно он работает, потому что язык к этому располагает. Потому что можно использовать ФВП, замыкания, и т.п. без всякого динамического полиморфизма.
G>>>А IEnumerable (foreach) с массивами действительно тормозит (накладные расходы на Enumerator оказываются больше, чем затратры на тело цикла). Как раз из-за недостатка инлайнинга, в C++ foreach нилайнится. EP>>Так дело не в самом foreach — ты если распишешь его вручную, то он также будет тормозить, за счёт динамического полиморфизма.. G>Для обычных массивов jit оптимизирует foreach
Вообще-то про foreach начал говорить ты, сначала сказал что тормозит, потом привёл пример где не тормозит. Круто чё.
Я же говорил про IEnumerable.
G>C IEnumerable другая проблема, которая связана с ФВП. Вообще ФП в C# довольно медленно работает, не создавался язык под него.
О чём и речь
G>>>Или ты имеешь ввиду использование ФВП для обхода массивов? Оно действительно и в C++, и в C# плохо работает. И в C++ и в C# такой код вручную оптимизируется. EP>>ФВП в том числе. А с чего ты взял что они в C++ плохо работают? Наоборот, прекрасно работают и отлично оптимизируются. EP>>Пример C++ -> JS vs C# как раз ФВП и использует. G>За счет инлайнинга, который JIT не делает. G>Но ты же не будешь в performance-critical на автоматический инлайнинг полагаться.
Так в том-то и фишка что буду, because I can! Зачем мне делать его руками, для всех используемых комбинаций алгоритмов * замыканий * последовательностей * прочих абстракций, когда с этим прекрасно справляется компилятор?
Там где это критично — проверю сгенерированный код, сделаю замеры — и добавлю простой тест с замерами на эту тему
G>>>>>Машинный код генерируется из IL. В нем все указанные тобой оптимизации делаются элементарно. G>>>>>Но у JIT нет столько времени на пребразования, как у компилятора C++. EP>>>>Что мешает сразу генерировать оптимизированный IL? G>>>IL — высокоуровневый язык. Он и так достаточно оптимален на своем уровне. EP>>И что из этого? C# версия без ФВП тоже компилируется в тот же самый "высокоуровневый IL", но работает быстрее. Версия с ФВП могла бы давать точно такой же IL. G>Не могла бы, потому что делегаты.
То есть таки язык мешает оптимизациям?
G>>>Работает быстро за счет "hotspot". Большинство движков JS перекомпилируют код на основании профиля использования. Кроме того "работает быстро" — это когда не используются ФВП. EP>>Вот именно, в исходном коде на C++ были ФВП и замыкание, в результирующем JS они были выоптимизированы компилятором C++, так как это элементарная операция (никакого динамического полиморфизма) — именно поэтому и быстро. G>Ну так ты продолжаешь доказывать, что проблема в языке C#.
Проблема в языке в том числе, за счёт повсеместного динамического полиморфизма и избыточных индерекций по памяти.