2014-02-05 4 views
5

Мне было любопытно, если мы наконец получим быструю библиотеку datetime с JDK8. Почти все вычисления LocalDate используют toEpochDay, поэтому я посмотрел на source, и большое количество подразделений и филиалов мне с любопытством, если бы я мог сделать лучше.Странное падение производительности JDK8 LocalDate.toEpochDay

results2

Я устранил некоторые ветвление и все, но одно деление, однако убыстрение хуже, чем ожидалось. Поэтому мой первый вопрос: как возможно, что алгоритм с использованием множественного деления занимает всего около 30 циклов (пропускная способность). Комментарии Хольгера, кажется, ответили на это: Подразделение по небольшой константе получает JIT-ed для умножения. Я сделал это вручную, и теперь я постоянно избивая первоначальную реализацию с коэффициентом 2.

benchmark довольно просто, просто итерация хотя массив случайных LocalDate с и преобразования каждого из них toEpochDay. Несмотря на случайность, results довольно последовательны. Размер массива - это параметр, и мой главный вопрос: , где большое замедление между 2000 и 30000 происходит от. Должно быть некоторое замедление, поскольку данные больше не вписываются в кеш-память L1, но обращения к памяти обоих алгоритмов абсолютно одинаковы (т. Е. Нет, но выбор date из массива).

Вопрос по-прежнему открытым: Как получается, что поведение двух простых реализаций без доступа к памяти с той же функцией изменяется при итерации по массиву? Исходный алгоритм страдает гораздо более сильным замедлением, чем мой.

ответ

0

thats, потому что в алгоритме нет делений. все/4 заменяются сдвигами. и все/100 на самом деле составляют 0,01. подразделения существуют для чтения (хе-хе). im не уверен, что эта оптимизация происходит во время выключения байт-кода или компиляции JIT, было бы интересно посмотреть на файл класса и узнать.

+0

В байтекоде нет такой оптимизации (javac ничего не делает в этом отношении). В общем случае '/ 4' не может быть заменено на' >> 2' (потому что это странная семантика разделения округления к нулю, здесь она может быть. Иногда вы можете заменить целочисленное деление на умножение и округление с плавающей запятой, m не уверен, если это поможет.Для общего длинного операнда это, скорее всего, невозможно, потому что 'double' имеет только 56-битную мантиссу. – maaartinus

+1

@maaartinus: если вы посмотрите на код, вы увидите' if (y> = 0) ' прямо перед делениями. Таким образом, JVM может доказать, что дивиденд не является отрицательным в одном случае, поэтому замена '/ 4' на' >> 2' применима, и даже в другом случае он может генерировать правильный код для его исправления, например a '- ((- y) >> 2)' будет делать, если вы знаете, что 'y' является отрицательным. – Holger

+1

Для деления на' 100' нет необходимости использовать с плавающей запятой.Поскольку исходные значения являются «int» и «short», а деление выполняется в «длинном» диапазоне, могут применяться оптимизации «целочисленное деление небольших чисел». [Википедия] (http://en.wikipedia.org/wiki/Division_algorithm#Division_by_a_constant) содержит краткое объяснение концепции. Проще говоря, '/ 100' заменяется на' * X/Y' на 'X' и' Y', так что 'Y' является степенью' 2' и, следовательно, '/ Y' заменяется сдвигом и' X/Y' * достаточно близко * к '0.01' для целых результатов. – Holger

1

Я не поймал причину напрямую, но это, безусловно, недостаток базового уровня. Что-то связано с расходами GC и per-invocation. У меня такая же деградация производительности с JMH, кроме скамейки со 100 датами, которая показывает лучший перфект, чем с 2000 датами. Я попытался создать массив dates с максимальным размером и перебрать только первые 100, 2000, 30000 элементов. В этом случае все версии выполнялись одинаково (15.3 + - 0.3 нс на моей машине).

import org.openjdk.jmh.annotations.*; 

import java.time.LocalDate; 
import java.util.*; 
import java.util.concurrent.ThreadLocalRandom; 
import java.util.concurrent.TimeUnit; 


@State(Scope.Benchmark) 
@BenchmarkMode(Mode.AverageTime) 
@OperationsPerInvocation(LocalDateBenchmark.ITERATIONS) 
@OutputTimeUnit(TimeUnit.NANOSECONDS) 
public class LocalDateBenchmark { 
    public static final int MAX_ITERATIONS = 1000000; 
    public static final int ITERATIONS = 30000; 

    private static final LocalDate MIN_DATE = LocalDate.of(1900, 1, 1); 
    private static final LocalDate MAX_DATE = LocalDate.of(2100, 1, 1); 
    private static final int DAYS_BETWEEN = (int) (MAX_DATE.toEpochDay() - MIN_DATE.toEpochDay()); 

    public LocalDate[] dates = new LocalDate[MAX_ITERATIONS]; 
    private Random random; 

    @Setup(Level.Trial) 
    public void setUpAll() { 
     Random r = ThreadLocalRandom.current(); 
     for (int i=0; i< dates.length; ++i) { 
      dates[i] = MIN_DATE.plusDays(r.nextInt(DAYS_BETWEEN)); 
     } 
    } 

    @Setup(Level.Iteration) 
    public void setUpRandom() { 
     random = new Random(); 
    } 

    @GenerateMicroBenchmark 
    public int timeToEpochDay(LocalDateBenchmark state) { 
     int result = 0; 
     LocalDate[] dates = state.dates; 
     int offset = random.nextInt(MAX_ITERATIONS - ITERATIONS); 
     for (int i = offset; i < offset + ITERATIONS; i++) { 
      LocalDate date = dates[i]; 
      result += date.toEpochDay(); 
     } 
     return result; 
    } 
} 
+0

Я не могу это подтвердить. Для меня [результаты] (https://microbenchmarks.appspot.com/runs/290b7154-4490-465f-a6e4-4364903410de) практически не изменились. Не могли бы вы опубликовать свой тест? – maaartinus

+0

@maaartinus см. Править. но теперь у меня есть версия-100 еще медленнее, чем другие ... – leventov

+0

Я вижу. Но это вы создаете недостающий кеш, сражаясь с предборником и, возможно, даже устраняете проверку диапазона. Это объясняет, почему 100 проигравших и, возможно, также почему скорость остается неизменной для других размеров. Тем не менее, я не думаю, что он объясняет в оригинальном случае (и я бы не назвал его фрейм-глюком, когда это случается как для суппорта, так и для JMH). – maaartinus

 Смежные вопросы

  • Нет связанных вопросов^_^