Так о том и речь, что копмилятор — это не волшебная коробка, которая сама во всем разберётся. Языковые конструкции и правила вроде приведенных в статье являются директивами JIT компилятору, чтобы он стал таким "умным" и гарантированно мог применять агрессивные оптимизации — в том числе inline-встраивание и преобразование final метода в статический вызов, — зная, что логика приложения после этого не нарушится.
Обычные виртуальные методы не могут работать ровно с такой же скоростью как статические просто в силу принципа их действия и устройства стековой машины вообще. Даже обращение к методу класса через интерфейсную ссылку накладывает дополнительные инструкции, чтобы машина могла определить какая реализаци интерфейса ассоциирована в данный момент с этой ссылкой и перепрыгнуть в соответствующую точку входа (класс->метод). Использование final как раз дает ей сведения о том, что далее в рантайме переменная никаким образом не может быть переопределена, поэтому можно смело можно избавиться от всякого динамизма и перекомпилировать участок кода в статический.
Речь здесь о микрооптимизациях, выгода от которых заметна в высоконагруженных системах на горячих участках кода и походить к этим вопросам из принципа "уберу, чтобы не пестрило в глазах" — такой себе вариант оптимизации.
В apache.commons-math есть Fraction и BigFraction, которые реализуют этот подход.
Но это не то, о чем пишет автор статьи. Здесь используется представление десятичной дроби в виде двух отдельных чиел — числителя и знаменателя для обыкновенной дроби. Никакой путаницы здесь не возникает, но и накладывает расходы на вычисления.
В статье же предлагается использовать один long, который просто представляет копейки. Этот подход самый оптимальный по производительности, но заморочен.
Вот бенчмарки Fraction в сравнении с BigDecimal:
@Benchmark public void fractionCall(Blackhole blackhole) {
Fraction result = new Fraction(1.2) .multiply(new Fraction(0.4)) .divide(new Fraction(0.8)) .multiply(new Fraction(0.6)) .add(new Fraction(1.25)) .divide(new Fraction(0.75));
Согласен с вашими доводами. Про странность совета погорячиться)
Тем не менее для обычных сервисов такая оптимизация все-таки избыточна и не сказать что клин код, т.к. внесет больше путанцы с округлением, чем прирост производительности в сравнении с BigDecimal.
Нечто подобное было во времена мидлетов в JavaME для кнопочных телефонов — там поначалу плавающей точки не было вообще и приходилось самим изобретать велосипед.
В общем советы полезные, но по поводу хранения денежных значений в long — как минимум странный.
BigDecimal ведь нужен не "для красоты при выводе", это прежде всего для корректности операций над числами с плавающей точкой. При использовании примитивов есть большая вероятность получить неожиданный результат, что весьма критично для денежных операций. Например, данное выражение будет ложным: 0.3 + 0.6 == 0.9, т.к. сумма будет равна 0.8999999999999999. Связано это не с конкретным языком программирования, а с особенностью представления чисел с плавающей точкой в двоичной системе счисления, где некоторые числа просто невозможно точно представить. BigDecimal как раз и призван решать эту проблему.
Для методов - вряд ли. Если брать прсто компилятор, то final методы он компилирует точно в такую же инструкцию invokevirtual, что и обычные методы. Это сделано для обратной совместимости с клиентским кодом, используюшим методоы, которые вдруг были перекомпилированы с удалением или добавлением final. Здесь в конце статьи описано подробнее: https://blogs.oracle.com/javamagazine/post/mastering-the-mechanics-of-java-method-invocation Что касается JIT компилятора, то у него есть механизмы оптимизации байткода и он все равно производит глобальный анализ дерева классов при загрузке. Хотя, возможно, final и является директивой для JIT в каких-то реализациях JVM, но и то только только на этапе анализа класса при загрузке. Но в любом случае к vtable это отношения не имеет. Так что касательно методов и классов final несет больше семантический смысл, чем прирост производительности.
Зато final полезно при объявлении статических полей класса. Т.к. в этом случае значения, указанные в static final поля при компиляции напрямую инлайнятся в стек операндов тех методов, которые его используют (например вызывается инструкция iconst_0 для интового поля), без предварительной инициализации класса. В противном случае, обращение к static полю без final компилируется в инструкцию getstatic, которая уже производит инициализацию класса перед обращением к полю, что, соответственно, занимает время и память.
Так о том и речь, что копмилятор — это не волшебная коробка, которая сама во всем разберётся. Языковые конструкции и правила вроде приведенных в статье являются директивами JIT компилятору, чтобы он стал таким "умным" и гарантированно мог применять агрессивные оптимизации — в том числе inline-встраивание и преобразование final метода в статический вызов, — зная, что логика приложения после этого не нарушится.
Обычные виртуальные методы не могут работать ровно с такой же скоростью как статические просто в силу принципа их действия и устройства стековой машины вообще. Даже обращение к методу класса через интерфейсную ссылку накладывает дополнительные инструкции, чтобы машина могла определить какая реализаци интерфейса ассоциирована в данный момент с этой ссылкой и перепрыгнуть в соответствующую точку входа (класс->метод).
Использование final как раз дает ей сведения о том, что далее в рантайме переменная никаким образом не может быть переопределена, поэтому можно смело можно избавиться от всякого динамизма и перекомпилировать участок кода в статический.
Речь здесь о микрооптимизациях, выгода от которых заметна в высоконагруженных системах на горячих участках кода и походить к этим вопросам из принципа "уберу, чтобы не пестрило в глазах" — такой себе вариант оптимизации.
В apache.commons-math есть Fraction и BigFraction, которые реализуют этот подход.
Но это не то, о чем пишет автор статьи. Здесь используется представление десятичной дроби в виде двух отдельных чиел — числителя и знаменателя для обыкновенной дроби. Никакой путаницы здесь не возникает, но и накладывает расходы на вычисления.
В статье же предлагается использовать один long, который просто представляет копейки. Этот подход самый оптимальный по производительности, но заморочен.
Вот бенчмарки Fraction в сравнении с BigDecimal:
@Benchmarkpublic void fractionCall(Blackhole blackhole) {Fraction result = new Fraction(1.2).multiply(new Fraction(0.4)).divide(new Fraction(0.8)).multiply(new Fraction(0.6)).add(new Fraction(1.25)).divide(new Fraction(0.75));blackhole.consume(result);}---
Benchmark Mode Cnt Score Error UnitsbigDecimalCall thrpt 150 3825.518 ± 53.397 ops/ms
bigFractionCall thrpt 150 114.674 ± 2.151 ops/ms
fractionCall thrpt 150 1717.929 ± 10.849 ops/ms
bigDecimalCall avgt 150 ≈ 10⁻⁴ ms/op
bigFractionCall avgt 150 0.009 ± 0.001 ms/op
fractionCall avgt 150 0.001 ± 0.001 ms/op
bigDecimalCall ss 150 0.030 ± 0.023 ms/op
bigFractionCall ss 150 0.143 ± 0.088 ms/op
fractionCall ss 150 0.022 ± 0.003 ms/op
Или же уловка языковой модели с целью внушить такое мнение читателю)
Согласен с вашими доводами. Про странность совета погорячиться)
Тем не менее для обычных сервисов такая оптимизация все-таки избыточна и не сказать что клин код, т.к. внесет больше путанцы с округлением, чем прирост производительности в сравнении с BigDecimal.
Нечто подобное было во времена мидлетов в JavaME для кнопочных телефонов — там поначалу плавающей точки не было вообще и приходилось самим изобретать велосипед.
В общем советы полезные, но по поводу хранения денежных значений в long — как минимум странный.
BigDecimal ведь нужен не "для красоты при выводе", это прежде всего для корректности операций над числами с плавающей точкой.
При использовании примитивов есть большая вероятность получить неожиданный результат, что весьма критично для денежных операций. Например, данное выражение будет ложным:
0.3 + 0.6 == 0.9, т.к. сумма будет равна0.8999999999999999. Связано это не с конкретным языком программирования, а с особенностью представления чисел с плавающей точкой в двоичной системе счисления, где некоторые числа просто невозможно точно представить. BigDecimal как раз и призван решать эту проблему.Для методов - вряд ли. Если брать прсто компилятор, то final методы он компилирует точно в такую же инструкцию
invokevirtual, что и обычные методы. Это сделано для обратной совместимости с клиентским кодом, используюшим методоы, которые вдруг были перекомпилированы с удалением или добавлением final. Здесь в конце статьи описано подробнее: https://blogs.oracle.com/javamagazine/post/mastering-the-mechanics-of-java-method-invocationЧто касается JIT компилятора, то у него есть механизмы оптимизации байткода и он все равно производит глобальный анализ дерева классов при загрузке. Хотя, возможно, final и является директивой для JIT в каких-то реализациях JVM, но и то только только на этапе анализа класса при загрузке. Но в любом случае к vtable это отношения не имеет.
Так что касательно методов и классов
finalнесет больше семантический смысл, чем прирост производительности.Зато
finalполезно при объявлении статических полей класса. Т.к. в этом случае значения, указанные вstatic finalполя при компиляции напрямую инлайнятся в стек операндов тех методов, которые его используют (например вызывается инструкцияiconst_0для интового поля), без предварительной инициализации класса. В противном случае, обращение кstaticполю безfinalкомпилируется в инструкциюgetstatic, которая уже производит инициализацию класса перед обращением к полю, что, соответственно, занимает время и память.Есть один замечательный канал по электронике для начинающих:
https://www.youtube.com/@HiDev
Там товарищ очень доходчиво объясняет принципы работы электронных компонентов и устройств.
Вот, в частности, видос про полупроводники:
https://www.youtube.com/watch?v=OMGdSCaMVD0&list=PL1s3wneoR_-on-07THWG5GFEZ-_mm-Pd2