При реализации метода compareTo, требуемого интерфейсом Comparable, я хочу использовать удобный метод Comparator, сгенерированный Comparator.comparing….
Например:
package work.basil.example.threading.multitasking;
import java.util.Comparator;
public record Job(
int id ,
int amt
) implements Comparable < Job >
{
@Override
public int compareTo ( final Job job )
{
return Comparator.comparingInt( Job :: id ).thenComparingInt( Job :: amt ).compare( this , job );
}
}
Будет ли вызов Comparator.comparingInt( … ).thenComparingInt( … ) создавать экземпляр Comparator при каждом выполнении этого метода? Или для этого будет оптимизирован компилятор Java байт-кода ( javac и т. д.) или JIT ( HotSpot , OpenJ9 и т. д.)?
Если компилятор не оптимизирует, я бы сделал константу private static final, вот так:
package work.basil.example.threading.multitasking;
import java.util.Comparator;
public record Job(
int id ,
int amt
) implements Comparable < Job >
{
private static final Comparator < Job > COMPARATOR = Comparator.comparingInt( Job :: id ).thenComparingInt( Job :: amt );
@Override
public int compareTo ( final Job job )
{
return Job.COMPARATOR.compare( this , job );
}
}
Я так не думаю. Это не похоже на то, что Comparator.comparing — это макрос или что-то в этом роде...
Вы спрашиваете, оптимизирует ли javac это? Или вы спрашиваете, оптимизирует ли это JIT? Если первое, то я сильно сомневаюсь. Не уверен насчет JIT.
Проведите сравнительный анализ и дайте нам знать о результатах.
Простите, если это очевидный вопрос, но вы смотрели исходный код (интерфейса Comparator)? [Статические] Метод comparing возвращает лямбду. Будет ли JIT создавать новую лямбду каждый раз при вызове метода?
@Slaw Либо компилятор байт-кода, либо JIT-компилятор. (Я отредактировал Вопрос для ясности.) Я просто хочу знать, стоит ли создание отдельной константы static final моего времени и усилий (для эффективной производительности выполнения) или это ненужное отвлечение внимания в коде. Я предполагаю, что да, мне следует сделать Comparator отдельной константой, чтобы избежать повторных экземпляров, но компиляторы настолько умны, что я никогда не могу быть полностью уверен в пределах их возможностей по оптимизации.
Что ж, ответ на вопрос «Будет ли Javac оптимизировать X?» почти всегда будет: «Нет». Что касается JIT, это сложнее понять. Это зависит от контекста вызова compareTo и того, насколько «горячим» compareTo во время выполнения. Вам придется оценить/профилировать его. Тем не менее, если вы сохраняете компаратор в статическом конечном поле, JIT вообще не нужно оптимизировать создание компаратора.




Да. Нет. Му. Зависит от того, что вы подразумеваете под «оптимизировать».
Javac находится на рельсах: он должен делать именно то, что говорит спецификация Java, не больше и не меньше, а спецификация Java включает в себя очень мало оптимизации. Спецификация Java в значительной степени гласит: «Этот Java-код превращается именно в этот байт-код, и вам не разрешено отклоняться от него, или вы не являетесь компилятором Java». В отличие, скажем, от компиляторов C, которые тратят массу времени на анализ потока кода и работу с настроенной целевой платформой, чтобы ввести абсурдные уровни оптимизации, такие как развертывание циклов, переписывание if для выполнения аннотированных предсказаний ветвей, встраивание методов, прямое исключение целых фрагменты кода, потому что этот путь кода согласно статическому анализу никогда не может быть затронут (а в мире тяжелого языка C это очень важная оптимизация, выполняемая компилятором!)
Множество руководств и в некотором смысле даже сама спецификация Java предполагают, что это:
list.forEach(x -> System.out.println(x.toLowerCase());
это просто синтаксический сахар для:
list.forEach(new Consumer<String>() {
@Override public void consume(String x) {
System.out.println(x.toLowerCase());
}
}
и для большинства намерений и целей это хороший способ объяснить это тем, кто знает, что такое анонимные литералы внутреннего класса (это #ifdef - синтаксис, используемый здесь), но это неверно именно в контексте вопроса Бэзила.
Поскольку анонимный внутренний литерал класса согласно спецификации должен быть скомпилирован в код, который создает экземпляр объекта, тогда как спецификация лямбда-выражений и ссылок на методы явно не требует этого и действительно изо всех сил старается указать, что «идентичность» «объекта», которым становится лямбда-выражение или ссылка на метод, не следует рассматривать как релевантный, поскольку javac/JVM оставляет за собой полное право сделать это бесполезным.
И действительно, это именно то, что он делает. Давайте скомпилируем ваш первый фрагмент, а затем запустим new Type() {...}, чтобы посмотреть, во что он скомпилирован:
> cat Job.java
... [omitted parts] ...
@Override
public int compareTo ( final Job job )
{
return Comparator.comparingInt( Job :: id ).thenComparingInt( Job :: amt ).compare( this , job );
}
> javac Job.java
> javap -c -v Job
.. [omitted _a lot_ of output] ..
public int compareTo(Job);
descriptor: (LJob;)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=2, args_size=2
0: invokedynamiC#16, 0 // InvokeDynamiC#0:applyAsInt:()Ljava/util/function/ToIntFunction;
5: invokestatic #20 // InterfaceMethod java/util/Comparator.comparingInt:(Ljava/util/function/ToIntFunction;)Ljava/util/Comparator;
8: invokedynamiC#26, 0 // InvokeDynamiC#1:applyAsInt:()Ljava/util/function/ToIntFunction;
13: invokeinterface #27, 2 // InterfaceMethod java/util/Comparator.thenComparingInt:(Ljava/util/function/ToIntFunction;)Ljava/util/Comparator;
18: aload_0
19: aload_1
20: invokeinterface #30, 3 // InterfaceMethod java/util/Comparator.compare:(Ljava/lang/Object;Ljava/lang/Object;)I
25: ireturn
LineNumberTable:
line 5: 0
Важнейшая вещь, на которую следует обратить внимание в этих выводах, — это то, что во всем этом блоке нет ни одной инструкции javap -c -v байт-кода (new создает новые объекты). В отличие от замены, например. new с длинной формой:
new ToIntFunction<Job>() {
@Override public int applyAsInt(Job job) {
return job.id();
}
}
и если мы Job::id получим совсем другие результаты:
stack=3, locals=2, args_size=2
0: new #16 // class Job$1
3: dup
4: aload_0
5: invokespecial #18 // Method Job$1."<init>":(LJob;)V
8: invokestatic #21 // InterfaceMethod java/util/Comparator.comparingInt:(Ljava/util/function/ToIntFunction;)Ljava/util/Comparator;
11: invokedynamiC#27, 0 // InvokeDynamiC#0:applyAsInt:()Ljava/util/function/ToIntFunction;
16: invokeinterface #31, 2 // InterfaceMethod java/util/Comparator.thenComparingInt:(Ljava/util/function/ToIntFunction;)Ljava/util/Comparator;
21: aload_0
22: aload_1
23: invokeinterface #34, 3 // InterfaceMethod java/util/Comparator.compare:(Ljava/lang/Object;Ljava/lang/Object;)I
28: ireturn
Обратите внимание, как здесь происходит javap -c -v, new относится к анонимному внутреннему классу, который здесь создается (код class Job$1, вызывается его конструктор (этот конструктор ничего не делает, и javac это знает, но поскольку javac находится на рельсах, это соответствует спецификация, необходимая для вставки этой инструкции new ToIntFunction<Job>() { .. }), даже несмотря на то, что этот конструктор является неактивным... на самом деле это не совсем пустая операция, он принимает в качестве аргумента invokespecial #18, поскольку это нестатический внутренний класс, который Javac знает, что он не используется. Опять же, несущественно, Job нужно выводить весь этот ненужный хлам, этого требует спецификация).
Возвращаясь к «хорошему» коду без javac, все, что происходит, — это набор инди-вызовов (indy — это сокращение от new и было довольно огромным дополнением к формату файла JVM/класса). Они вызывают метод «#0:applyAsInt», который не принимает аргументов (InvokeDynamic) и возвращает экземпляр () (это то, что представляет собой ToIntFunction). Это 'создает значение типа ToIntFunction, но само по себе не выполняет эту работу, вместо этого он вызывает какую-то туманную вещь с InDy, чтобы создать это значение.
Так что же здесь происходит? Нам нужно прокрутить вывод Ljava/util/function/ToIntFunction; до конца, чтобы получить больше информации:
BootstrapMethods:
0: #77 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#65 (Ljava/lang/Object;)I
#66 REF_invokeVirtual Job.id:()I
#69 (LJob;)I
1: #77 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#65 (Ljava/lang/Object;)I
#70 REF_invokeVirtual Job.amt:()I
#69 (LJob;)I
2: #84 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
Method arguments:
#8 Job
#73 id;amt
#75 REF_getField Job.id:I
#76 REF_getField Job.amt:I
InnerClasses:
public static final #96= #92 of #94; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
Вот где происходит волшебство. Эти «методы начальной загрузки» кажутся совершенно сумасшедшими, что, черт возьми, такое LambdaMetaFactory, что такое MethodHandles$Lookup, почему в огне есть упоминание внутреннего класса, при компиляции этого кода получается всего один файл javap вместо обычного файла class мы ожидаем увидеть, когда напишем настоящий внутренний класс (и который действительно появится, если мы напишем анонимный литерал внутреннего класса и скомпилируем его, тогда мы получим Job$Inner.class)?
Эти оптимизации были введены в Java8 (на самом деле, Job$1.class как инструкция байт-кода была введена в OpenJDK7, но invokedynamic не генерировала инструкции InDy до Java8). Объяснение того, как именно это работает, довольно сложно и требует полного погружения в то, что делает инди, но, подведем итог по этому конкретному вопросу: по сути, вся эта шумиха делает именно то, что вы хотите: она делает «стоимость» этой однострочной фразы идентичной. иметь отдельную константу.
Таким образом, вывод прост: нет, вам НЕ нужно переписывать ваш простой фрагмент во вторую форму; не вводите константу, быстрее не будет. Во всяком случае, это будет медленнее.
Эти методы начальной загрузки вызываются «по мере необходимости», но в этом случае, по сути, только один раз, результаты их применения привязываются к соответствующей инструкции javac, так что метод начальной загрузки не нужно запускать каждый раз. Это настолько близко к тому, что «я создал константу, чтобы избежать каждого вызова моего invokedynamic метода, создающего объекты и запускающего кучу кода», насколько это возможно, особенно включая то, что он не создает никакого мусора, даже кратковременного мусора.
Я рекомендую эту статью в Java-журнале Oracle о InvokeDynamic, если вы хотите точно знать, как это работает. Здесь также объясняется, что такое методы начальной загрузки и как InDy их использует.
Судя по результатам тестов, постоянный подход работает практически так же, как непостоянный, как по распределению, так и по среднему времени выполнения — если метод compareTo встроен в JIT. Если бы не встроенный, то постоянный подход был бы «лучше». Или, по крайней мере, подход с константами показал лучшие результаты в тестах с -XX:-Inline (это портит работу с ignoredynamic?). Тем не менее, любой контекст «реального мира», где разница будет иметь значение, скорее всего, compareTo будет «горячим» и, следовательно, будет встроенным.
что говорит javap о скомпилированном файле класса?