Pull to refresh

Comments 22

@ruslan_astratov спасибо за обратную связь!
Какие варианты оптимизации открыли для себя?
Удалось применить их в деле?

В общем советы полезные, но по поводу хранения денежных значений в long — как минимум странный.

BigDecimal ведь нужен не "для красоты при выводе", это прежде всего для корректности операций над числами с плавающей точкой.
При использовании примитивов есть большая вероятность получить неожиданный результат, что весьма критично для денежных операций. Например, данное выражение будет ложным: 0.3 + 0.6 == 0.9, т.к. сумма будет равна 0.8999999999999999. Связано это не с конкретным языком программирования, а с особенностью представления чисел с плавающей точкой в двоичной системе счисления, где некоторые числа просто невозможно точно представить. BigDecimal как раз и призван решать эту проблему.

так идея-то в том, чтобы не использовать дробные типы. Считать в копейках. тогда 30+60 будет равно 90. а в выводе ендюзеру покажете 0,9 руб )

а теперь подумаем над операцией деления.
мне нужно пересчитывать юани в рубли и обратно по курсу 1 : 11.3
;)

скейл ап. установить точность единую для всех валют. 6 или 9. и тогда любое деление будет проходить в рамках целых чисел. Так лампорты работают в солана, например.
опять же, предложенные в сабже методы оптимизаций дают заметный эффект на высоконагруженных системах. Если в стандартном бизнес сервисе применять эти подходы - пользователи и метрики вряд ли обнаружат значительные изменения. Я думаю, что это как клин код для обычных и маст хэв для высоконагруженных систем.

Согласен с вашими доводами. Про странность совета погорячиться)

Тем не менее для обычных сервисов такая оптимизация все-таки избыточна и не сказать что клин код, т.к. внесет больше путанцы с округлением, чем прирост производительности в сравнении с BigDecimal.

Нечто подобное было во времена мидлетов в JavaME для кнопочных телефонов — там поначалу плавающей точки не было вообще и приходилось самим изобретать велосипед.

это прекрасное предложение! только придется всю "арифметику" для них реализовать.
либо воспользоваться готовой библиотекой.
но будет ли этой быстрее BigDecimal? кто-то проверял?

В 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));

blackhole.consume(result);
}


---

Benchmark Mode Cnt Score Error Units
bigDecimalCall 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

вообще, это, конечно рабочий вариант. мы, действительно, можем считать, что последние 6-9 знаков в long - это наша дробная часть, а все, что левее - целая часть. и, при этом, все хранить в "копейках". тогда мы всегда можем делать одинаковое округление в последнем знаке и оставаться в рамках целых чисел.
но тут нас ожидает другая проблема: у нас не такой уж большой максимум.
сделки на "сотни миллионов" потребуют отдельной "ветки" в логике обработки, которая будет работать медленнее основной.
а ведь "сотни миллионов" - это не так уж и много.
в общем, предложенная "оптимизация" находится где-то сильно справа на кривой "шипилева".
туда стоит лезть только будучи на 100% уверенным, что "тормозит" именно арифметика, а не условный I/O

@alexander_pesh , @Pdasilem , @Siemargl , @GreyN , спасибо за дискуссию!

В данной статье была цель предоставить варианты оптимизации кода, в том числе и Правило №7. BigDecimal vs Long (Битва за примитивы).

Действительно, если проект находится на этапе реализации, то смело используйте BigDecimal. Выше указали основные аргументы почему стоит использовать BigDecimal и какие вопросы он решает + накину свои:

  • Проблема деления (Scale Up)

  • Проблема переполнения (Overflow)

  • Сложность поддержки. Необходим дополнительный контроль в большой команде с разной экспертизой. Банальная проблема - есть риск забыть привести данные к нужному формату

  • Стандартное бизнес-приложение, много операций делений/процентов

По результатам нагрузочного тестирования можно будет сформировать понимание - необходимо ли производить оптимизации.

Когда стоит сразу задуматься об использовании денежных значений в минимальных единицах (long / int):

  • High-load

  • Критичен бюджет аллокаций (жёсткий лимит ресурсов)

  • Пишите ядро торгового движка/блокчейна

Автор написал, что хранить в копейках например.

Это вполне корректный вариант, и не нужен bigdecimal, и все операции происходить с целыми числами, тогда не будет никаких неожиданностей.

у меня курс 1 к 3
(1 "доллар" за 3 "рубля")
сколько "долларов" я могу получить за 10 "рублей"?
(ответ 333 "цента" - совершенно не верный)

3 доллара или 3 доллара 33 цента. перед выводом пользователю, если надо, убирай все после точки или не удаляй. это ж бизнес логика. Деление целых чисел вернет именно 333

Советы полезные.

Но сам GraalVM - штука спорная, имхо. Стоит прибегать, только если вообще никаких других вариантов не осталось.

Java ведь изначально задумалась как прямо противоположное тому, что делает GraalVM (один код работает везде, без разных бинарников под каждую платформу). И теперь, когда сову попытались натянуть на глобус, внезапно оказывается, что постоянно всплывают неудобные моменты.

Уж если нужен бинарник, пожалуй, стоит использовать язык, который под это и создавался. Тот же Go.

@remindscope отличная точка для дискуссии!
Если вопрос в написании нового решения в 2026 году, есть эксперты в команде по Go - почему бы и не написать на языке Go? Да конечно можно!
И с философией Java так же правы - пиши один код и запускай везде!

GraalVM - это не сова, это вынужденная адаптация Java под современные реалии облаков (Cloud Native). В данном случае Java просто эволюционирует, чтобы не проиграть Go или Rust в нише маленьких контейнеров. При этом концепция запуска бинарника под каждую платформу также сохраняется, но с некоторой "эволюцией" - запуск внутри контейнера.

Контейнеризация заставляет и главное, что позволяет большинству технологий прогрессировать и меняться в лучшую сторону.

"Правило №3. Final везде"

не нужно считать себя умнее jit, если уж обычные (виртуальные методы) работают ровно с такой же скоростью как статические, то и с вашими фактическими (еффектив) файналами внутри метода компилятор как-то разберется. Лично я их всегда удаляю внутри метода, чтоб в глазах не пестрило.

Так о том и речь, что копмилятор — это не волшебная коробка, которая сама во всем разберётся. Языковые конструкции и правила вроде приведенных в статье являются директивами JIT компилятору, чтобы он стал таким "умным" и гарантированно мог применять агрессивные оптимизации — в том числе inline-встраивание и преобразование final метода в статический вызов, — зная, что логика приложения после этого не нарушится.

Обычные виртуальные методы не могут работать ровно с такой же скоростью как статические просто в силу принципа их действия и устройства стековой машины вообще. Даже обращение к методу класса через интерфейсную ссылку накладывает дополнительные инструкции, чтобы машина могла определить какая реализаци интерфейса ассоциирована в данный момент с этой ссылкой и перепрыгнуть в соответствующую точку входа (класс->метод).
Использование final как раз дает ей сведения о том, что далее в рантайме переменная никаким образом не может быть переопределена, поэтому можно смело можно избавиться от всякого динамизма и перекомпилировать участок кода в статический.

Речь здесь о микрооптимизациях, выгода от которых заметна в высоконагруженных системах на горячих участках кода и походить к этим вопросам из принципа "уберу, чтобы не пестрило в глазах" — такой себе вариант оптимизации.

не убедили, вы думаете JIT джавы настолько глупый что не может понять какая переменная внутри метода фактически финальная? Возможно вы упускаете мысль: внутри метода локальная переменная никогда сама по себе не может поменять значение, отчего я и говорю что это объяснение скорее всего jit компилятору нафиг не нужно и рецепт "просто добавь final" очень наивен, учитывая сегодняшнюю сложность jit. Другое дело если мы говорим о полях класса, там конечно не помешает.

Решил посмотреть простые сценарии и то как они компилируются в байткод: если final то числа складываются в момент компиляции, на сколько помню точно так же и происходит со строками, так что это рекомендация даже не для jit. Ожидал что и не final тоже вычислится.

gemini говорит что я прав: это бородатая байка, потому что в байткоде нет никакого флага final для локальных переменных и далее его речь:

Любой современный JIT или AOT компилятор первым делом переводит твой байткод во внутреннее представление, которое называется SSA-форма (Static Single Assignment).
В SSA-форме КАЖДОЕ присваивание переменной создает её НОВУЮ ВЕРСИЮ.

Если ты написал:

int x = 5;
x = 10;

В SSA-графе компилятора это превратится в:

x_1 = 5;
x_2 = 10;

Для компилятора ЛЮБАЯ локальная переменная в SSA-графе является финальной (константной) по своей математической природе! Ему не нужны твои подсказки в виде слова final. Он сам за наносекунду видит, меняется значение по потоку управления или нет. Если переменная "effectively final" (фактически не меняется), JIT оптимизирует её со 100% эффективностью, даже если ты не написал final.

Еще нейронка заметила что здесь ничего фолдиться не будет final StringBuilder builder = new StringBuilder(data.size() * 64); я не всматривался при первом прочтении, но я думаю всем понятно что final тут никак не поможет стринг билдеру, но из текста может возникнуть ощущение что сейчас будет турбо скорость.

@yrub, снимаю шляпу! Про SSA-форму - в точку!
Внёс уточнение в статью, чтобы не плодить мифы.

Sign up to leave a comment.

Articles