Статьи

Если BigDecimal является ответом, это должен быть странный вопрос

обзор

Многие разработчики определили, что BigDecimal — единственный способ иметь дело с деньгами. Часто они заявляют, что, заменяя double на BigDecimal, они исправляют одну или десять ошибок. Что я нахожу неубедительным в этом, так это то, что, возможно, они могли бы исправить ошибку в обработке double и что дополнительные издержки использования BigDecimal.

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

BigDecimal не является улучшением

У BigDecimal много проблем, так что выбирайте сами, но уродливый синтаксис, пожалуй, худший грех.

  • Синтаксис BigDecimal неестественный.
  • BigDecimal использует больше памяти
  • BigDecimal создает мусор
  • BigDecimal намного медленнее для большинства операций (есть исключения)

Следующий тест JMH демонстрирует две проблемы с BigDecimal, ясность и производительность.

Код ядра принимает в среднем два значения.

Двойная реализация выглядит следующим образом. Примечание: необходимо использовать округление.

1
mp[i] = round6((ap[i] + bp[i]) / 2);

Та же самая операция, использующая BigDecimal, не только длинная, но и много кода для навигации.

1
2
mp2[i] = ap2[i].add(bp2[i])
     .divide(BigDecimal.valueOf(2), 6, BigDecimal.ROUND_HALF_UP);

Это дает вам разные результаты? double имеет 15 цифр точности, а числа намного меньше 15 цифр. Если бы эти цены имели 17 цифр, это бы сработало, но не сработало бы и для бедного человека, который также должен понимать цену (то есть они никогда не будут невероятно длинными).

Производительность

Если вам приходится нести затраты на кодирование, обычно это делается из соображений производительности, но здесь это не имеет смысла.

эталонный тест Режим образцы Гол Ошибка в счете Единицы измерения
osMyBenchmark.bigDecimalMidPrice thrpt 20 23638.568 590,094 OPS / с
osMyBenchmark.doubleMidPrice thrpt 20 123208.083 2109.738 OPS / с

Вывод

Если вы не знаете, как использовать round в double, или ваш проект требует BigDecimal, тогда используйте BigDecimal. Но если у вас есть выбор, не просто предполагайте, что BigDecimal — верный путь.

Код

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
 
import java.math.BigDecimal;
import java.util.Random;
 
@State(Scope.Thread)
public class MyBenchmark {
    static final int SIZE = 1024;
    final double[] ap = new double[SIZE];
    final double[] bp = new double[SIZE];
    final double[] mp = new double[SIZE];
 
    final BigDecimal[] ap2 = new BigDecimal[SIZE];
    final BigDecimal[] bp2 = new BigDecimal[SIZE];
    final BigDecimal[] mp2 = new BigDecimal[SIZE];
 
    public MyBenchmark() {
        Random rand = new Random(1);
        for (int i = 0; i < SIZE; i++) {
            int x = rand.nextInt(200000), y = rand.nextInt(10000);
            ap2[i] = BigDecimal.valueOf(ap[i] = x / 1e5);
            bp2[i] = BigDecimal.valueOf(bp[i] = (x + y) / 1e5);
        }
        doubleMidPrice();
        bigDecimalMidPrice();
        for (int i = 0; i < SIZE; i++) {
            if (mp[i] != mp2[i].doubleValue())
                throw new AssertionError(mp[i] + " " + mp2[i]);
        }
    }
 
    @Benchmark
    public void doubleMidPrice() {
        for (int i = 0; i < SIZE; i++)
            mp[i] = round6((ap[i] + bp[i]) / 2);
    }
 
    static double round6(double x) {
        final double factor = 1e6;
        return (long) (x * factor + 0.5) / factor;
    }
 
    @Benchmark
    public void bigDecimalMidPrice() {
        for (int i = 0; i < SIZE; i++)
            mp2[i] = ap2[i].add(bp2[i])
            .divide(BigDecimal.valueOf(2), 6, BigDecimal.ROUND_HALF_UP);
    }
 
 
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(".*" + MyBenchmark.class.getSimpleName() + ".*")
                .forks(1)
                .build();
 
        new Runner(opt).run();
    }
}