Comments 46
UPD. Можно, конечно, оставить и имеющееся, ради прикола, но тогда переделать 1-й абзац.
После стольких лет существования дженериков в Java есть ещё не знакомые с темой?
За столько лет существования рифм уже написано много статей, книг и прочей литературы с полным разбором всего и вся.
И для Java Generics ситуация ровно такая же: есть и вводные курсы и подробное описание всё есть.
Зачем оно тут? Я вот даже не уверен что это 100% авторский контент — есть ощущение что эти примеры я уже видел.
Java Generics появились в JDK 1.5, которая вышла 30 сентября 2004 года — через пару месяцев им будет 14 лет. Знаете когда эта статья была полезна? 14 лет назад — вообще must have, ну лет 10 назад (хотя и так уже было много литературы).
А сейчас?
Есть свежие технологии — да хотя бы лямбды — им всего 4 года (четыре, не четырнадцать).
Ну и по ним уже есть много литературы, как англоязычной, так и русскоязычной.
А использование переменной работает и на «холодном» коде тоже.
Сейчас так конечно никто не пишет. Зачем? ведь есть же foreach. И это верно! Всегда используйте foreach вместо for, если коллекция реализует интерфейс Iterable. Но в далекие времена Java 2.0 не было Iterable.
А что значит «оптимизация имеет смысл»?
Значит, что код от оптимизации будет работать быстрее. Ну или хотя бы выделять меньше памяти.
Я думаю тут есть смысл.
Нет, если нет ускорения работы программы.
Вы же и без меня знаете, что оптимизаторы в Java действительно хорошо работают. Они умеют инлайнить методы, создавать объекты на стеке, сворачивать известные конструкции. В частности, при итерировании по массиву JIT умеет находить знакомые циклы и убирать проверку выхода за границу массива. JIT умеет "девиртуализовывать" методы и делать еще тьму важных моментов.
Я не понимаю, ну почему Вы спорите… Ну если будет код работать быстрее — так приведите доказательство, что тут сложного? Или скажите, что доказательств нет. Вы же в Сбертехе работаете, должны ведь уметь делать тесты на производительность. Вы можете всего лишь форкнуть репозиторий, а в нем сделать две реализации — с вашей идей и без. Разве это сложно?
Вы не правы. Вообще-то правильно выносить вычисления, чтобы сделать их один раз, чем делать их много раз.
Ну и про инлайн тут вообще при чем? Если тут и идёт о чем-то речь, то о подстановке результата вызова функции как значения, но есть разные вещи, когда такие оптимизации могут быть недопустимы и при большом значении N, накладные расходы могут быть значимы.
Так что, приведенный код более корректный, и вообще, лучше сразу писать правильно, чем надеяться, что что-то будет оптимизироваться… Да и при переходе необходимости использовать другой язык — это тоже полезная идея.
Вообще-то правильно выносить вычисления, чтобы сделать их один раз, чем делать их много раз.
Пруф? Или это Ваше мнение?
при большом значении N
Вы говорите про алгоритмическую сложность? Мы же обсуждаем, что константа (в терминах O(...)) одинакова как для функции с оптимизацией, так и для функции без неё.
Ну и про инлайн тут вообще при чем?
Если мы говорим про оптимизацию производительности, то хотелось бы видеть обоснование — почему добавление строк кода (а зачастую это ведет к созданию менее читаемого кода) дает прирост производительности? Какой прирост тогда будет?
Всё дело в том, что замерить производительность не просто, а очень просто.
Смотрите:
- Шаг 1: скачиваете код с проектом, который меряет производительность. Например, этот.
- Шаг 2: пишете два варианта функции: с оптимизацией и без
- Шаг 3: запускаете замеры из командной строки
./gradle jmh
На всё про всё — максимум 30 минут, учитывая, что еще надо поставить JRE и IntelliJ Idea Community.
А теперь, вопрос: если при такой простоте замеров нет доказательств производительности, то есть ли вообще ускорение? Может, всем и так известно, что прироста нет никакого, потому никто и не публикует замер?
Если виртуальная машина поддерживает и сможет доказать возможность девиртуализации, то разницы между n = list.size() и i < list.size() нету.
Если же мы, например, передадим в качестве параметра экземпляр SynchronizedList, то разница будет.
Да и зацепились вы за пример из проекта написанного в эпоху динозавров. В то время HotSpot не умел выполнять такого рода оптимизации.
зацепились вы за пример
Я зацепился на преждевременную оптимизацию. Если в статье есть примеры кода в стиле "как можно", то хорошо бы вычищать их от подобных "советов".
Если же мы, например, передадим в качестве параметра экземпляр SynchronizedList, то разница будет.
А какая? Код будет работать в 10 раз быстрее? Или в 10 раз медленнее? Зачем усложнять код на ровном месте-то?
Я понимаю что бремя доказательства лежит на Marvinorez, а не на вас и вам доказывать ничего не нужно.
Хотя именно у вас есть опыт в создании JMH-тестов — могли бы и помочь человеку, но не захотели — бывает :)
Я решил помочь. Вот тесты производительности, вот их сырые результаты — если я что-то не то или не так тестировал — милости просим в PR.
И вот табличка с результатами (Habr не смог переварить такую большую таблицу :( ).
И я, из результатов тестов, вижу что кеширование размера коллекции может давать профит, но в зависимости от типа коллекции:
java.util.HashSet
— выгодноjava.util.TreeSet
— не выгодноjava.util.ArrayList
— выгодноjava.util.LinkedList
— выгодно
Что действительно нужно сравнивать — так это ArrayList и обычные массивы.
большинство проверок — лишние.
Я это понимаю — я тут заодно прокачивал свой навык написания бенчмарков на JMH :)
Что действительно нужно сравнивать — так это ArrayList и обычные массивы.
Начальный вопрос этого треда состоял в вопросе к преждевременной оптимизации
в виде кеширования размера массива в переменной, локальной для цикла.
На сколько это неаобходимо и есть ли от этого профит в принципе.
Сильно подозреваю что доступ к элементу массива будет быстрее доступа к элементу ArrayList
-а, но опять же копеечно и только из-за того что каждый вызов ArrayList#get(int)
это:
- собственно вызов метода
- проверка выхода за правую границу
- вызов внутреннего метода для каста
- обращение к массиву по индексу
Просто обращение к массиву по индексу
будет явно быстрее из-за отсутствия всех этих вещей, как бы их JVM не инлайнила.
Да я и сейчас не понял.
Вы предлагаете узнать профит от кеширования размера коллекции при обходе new int[]{1, 2, 3}
и Arrays.asList(1, 2, 3)
?
Бенчмарк, сырые данные, табличка, вывод: с кешем быстрее.
Возможно сказывается то что кешированный размер лежит в локальной переменной, а не достаётся через обращения к полю/методу другого объекта.
Наткнулся в видео на аналогичный пример в .Net, правда с массивом.
Но разница значительная по скорости, но в обратную сторону. .Net увидев паттерн сравнения с длиной массива в цикле, убирает из IL проверки на выход за пределы массива.
В общем, вывод, что нужно знать платформу на которой пишешь.
Но все же спрошу в ответ, готовы ли вы поручиться, что эта оптимизация никогда не имеет смысла, включая следующие случаи:
1. использование AOT-компилятора вместо JIT;
2. частый холодный запуск утилиты;
3. запуск на Android с его Davlik;
4. запуск под IKVM.NET на Unity с его устаревшим форком Mono в браузере через WebAssembly;
5. запуск в браузере через GWT;
6. запуск на Java Card?
Кстати, поддерживает ли JMH все перечисленные мною сценарии?
Я вспомнил, где видел данные примеры: Доклад А.Маторина «Неочевидные дженерики» на JPoint и JBreak 2016.
Но разве это не то, что ожидается? Т.е. если вместо списка объектов типа Account передали список, например, Employee, то программа должна упасть и как можно громче.
В сигнатуре метода нет никакого упоминания о том, что список должен содержать только объекты класса Account. Возможна ситуация когда нам передали список из множества объектов типа Account и Employee или список содержащий элемент равный null. По хорошему, конечно, об этом нужно писать в комментарии и это был единственный способ рассказать о том, что же ожидает метод до появления Generics.
То что она должна упасть как можно громче… Эммм… не всегда это верно. Программа может упасть с грохотом на этапе валидации — это хорошо, это fail-fast. Но что, если программа у вас падает где-то глубоко в бизнес логике, где падение может привести к неконсистентным данным, незавершенным транзакциям и т.д. — это уже не fail-fast.
Да еще и с NPE упадет если null передать. Получается одно проверили, а другое забыли.да, это хорошее замечание. Проверка на null там не будет лишней.
Да запросто. В Java можно написать Collection<? extends Foo>
, и это будет автоматически работать для любой коллекции — а в C# для этой цели пришлось придумывать отдельный интерфейс IReadOnlyCollection<Foo>
, который, конечно же, никакой класс автоматически реализовывать не начал.
К примеру, в .net 4.5 класс HashSet почему-то не реализовывал этот интерфейс...
Сомнительное "будет работать". Метод add есть, но вызвать его нельзя. По мне так это довольно абсурдно. C IReadOnlyCollection же всё понятно.
К примеру, в .net 4.5 класс HashSet почему-то не реализовывал этот интерфейс...
Что за гнусное вранье, зачем вы обманываете IL_Agent?
Вот ссылка HashSet на MSDN, он реализует IReadOnlyCollection.
И работает эта конструкция ровно так, как и ожидается: если A наследует B, то вместо IReadOnlyCollection<B>
можно передавать IReadOnlyCollection<A>
. В .Net еcть ковариантность и контрвариантность на уровне компилятора.
Вот ссылка HashSet на MSDN, он реализует IReadOnlyCollection.
… начиная с 4.6
И работает эта конструкция ровно так, как и ожидается: если A наследует B, то вместоIReadOnlyCollection<B>
можно передаватьReadOnlyCollection<A>
.
А вот для ICollection<>
такого нету. В отличии от Java, где для любого интерфейса можно автоматически получить его ковариантную и контравариантную части.
Пришел, увидел, обобщил: погружаемся в Java Generics