Является ли использование собственной емкости int быстрее, чем использование поля .length массива?
В 95% производительности - это чистые представительские модели беседует Мартин Томпсон, между 17 и 21 минутами, представлен такой код:
public class Queue
{
private final Object[] buffer;
private final int capacity;
// Rest of the code
}
В 20:16 он говорит:
Вы можете получить гораздо лучшую производительность, поэтому оставляя такие вещи, как capacity
в этом есть правильная вещь.
Я попытался придумать пример кода, в котором capacity
будет намного быстрее, чем buffer.length
, но я потерпел неудачу.
Мартин говорит, что проблемы возникают в двух сценариях:
- В параллельном мире. Но поле
length
также final
, JLS 10.7. Итак, я не вижу, как это может быть проблемой.
- Когда кеш промахивается. я попробовал, вызвав
capacity
vs buffer.length
миллион раз (с очередь, имеющая миллион элементов), но существенной разницы не было. Я использовал JMH для бенчмаркинга.
Не могли бы вы привести пример кода, который демонстрирует случай, когда capacity
превосходит buffer.length
с точки зрения производительности?
Чем более распространенный случай (часто замеченный в реальном коде), тем лучше.
Обратите внимание, что я полностью отнимаю аспект эстетики, чистый код, потенциал для повторного факторинга кода и т.д. Я прошу только о производительности.
Ответы
Ответ 1
Когда вы обычно обращаетесь к массиву, JVM использует его length
в любом случае для выполнения проверки границ. Но когда вы обращаетесь к массиву через sun.misc.Unsafe
(как это делает Мартин), вам не нужно платить это неявное наказание.
Поле Array length
обычно находится в той же строке кэша, что и его первые элементы, поэтому у вас будет ложный обмен, когда несколько потоков записываются в первых индексов одновременно. Использование отдельного поля для емкости буфера приведет к нарушению этого ложного обмена.
Вот эталон, показывающий, как поле capacity
делает доступ к массиву значительно быстрее:
package bench;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicReferenceArray;
@State(Scope.Benchmark)
@Threads(4)
public class Queue {
private static final Unsafe unsafe = getUnsafe();
private static final long base = unsafe.arrayBaseOffset(Object[].class);
private static final int scale = unsafe.arrayIndexScale(Object[].class);
private AtomicReferenceArray<Object> atomic;
private Object[] buffer;
private int capacity;
@Param({"0", "25"})
private volatile int index;
@Setup
public void setup() {
capacity = 32;
buffer = new Object[capacity];
atomic = new AtomicReferenceArray<>(capacity);
}
@Benchmark
public void atomicArray() {
atomic.set(index, "payload");
}
@Benchmark
public void unsafeArrayLength() {
int index = this.index;
if (index < 0 || index >= buffer.length) {
throw new ArrayIndexOutOfBoundsException();
}
unsafe.putObjectVolatile(buffer, base + index * scale, "payload");
}
@Benchmark
public void unsafeCapacityField() {
int index = this.index;
if (index < 0 || index >= capacity) {
throw new ArrayIndexOutOfBoundsException();
}
unsafe.putObjectVolatile(buffer, base + index * scale, "payload");
}
private static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new AssertionError("Should not happen");
}
}
}
Результаты:
Benchmark (index) Mode Cnt Score Error Units
Queue.atomicArray 0 thrpt 5 41804,825 ± 928,882 ops/ms
Queue.atomicArray 25 thrpt 5 84713,201 ± 1067,911 ops/ms
Queue.unsafeArrayLength 0 thrpt 5 48656,296 ± 676,166 ops/ms
Queue.unsafeArrayLength 25 thrpt 5 88812,863 ± 1089,380 ops/ms
Queue.unsafeCapacityField 0 thrpt 5 88904,433 ± 360,936 ops/ms
Queue.unsafeCapacityField 25 thrpt 5 88633,490 ± 1426,329 ops/ms
Ответ 2
Вы не должны воспринимать слова Мартина напрямую. Когда он сказал: "Использование array.length
- это анти-шаблон, который копируется по проектам", я думаю, это лукаво.
Использование поля capacity
действительно позволяет улучшить локальность, меньше загрязняет кэши и помогает избежать ложного обмена, но для этого требуется написать действительно ужасный исходный код, который очень далек от того, чтобы быть "чистым и простым", Мартин рекламировал в этот разговор.
Проблема в том, что даже вы не пишете array.length
в своем источнике напрямую, JVM в любом случае получает доступ к длине (это значит, обращается к заголовку массива) для каждого индексации массива array[i]
, чтобы проверить границы. Hotspot JVM имеет проблемы с устранением проверок границ даже в "простых" случаях циклов, и я думаю, что он не может интерпретировать некоторые "внешние" проверки, такие как if (i < capacity) return array[i];
, как связанная проверка, т.е. е. свяжите поле емкости и размер массива.
Вот почему, чтобы сделать capacity
-pattern имеющим смысл, вам нужно получить доступ к массиву только через Unsafe
! Это, к сожалению, отключает многие оптимизации массового цикла.
Посмотрите на реализацию "чистой" очереди Мартина:)
Я также мог бы попытаться объяснить, что подразумевалось под параллельными соображениями при доступе к "окончательному" array.length
. Мои эксперименты показывают, что даже "чтение-чтение" параллельного доступа к кеше вводит какой-то "ложный обмен" и замедляет работу. (Я думаю, что инженеры JVM рассмотрели это, когда @sun.misc.Contended
выполнили смещение 128 байт с обеих сторон разрешенных полей, вероятно, это должно обеспечить как предварительную выборку двух сторон кеш-кеши, так и "чтение-чтение ложного обмена" не повлияет на производительность.)
Вот почему, когда потребители и производители очереди получают доступ к емкости для обертывания вокруг буфера, они лучше получают доступ к различным объектам, содержащим одно и то же поле (по значению) capacity
и ссылку на тот же массив. Доступ к этому массиву через небезопасных производителей и компиляторов обычно обеспечивает доступ к различным областям этого массива, не распространяйте ничего ложно.
IMO антипаттерн теперь должен попытаться реализовать еще один Queue
, в то время как люди, стоящие за https://github.com/JCTools/JCTools (включая Martin, btw), оптимизируют это до смерти.
Ответ 3
Я не эксперт JVM и не претендую на понимание его оптимизации.
Рассматривали ли вы просмотр байтового кода, чтобы узнать, какие инструкции выполняются?
public class Queue {
private final Object[] buffer;
private final int capacity;
public Queue(int size) {
buffer = new Object[size];
this.capacity = size;
}
public static void main(String... args) {
Queue q = new Queue(10);
int c = q.capacity;
int l = q.buffer.length;
}
}
Это дизассемблированный байт-код для основного метода выше.
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=3, locals=4, args_size=1
0: new #5 // class Queue
3: dup
4: bipush 10
6: invokespecial #6 // Method "<init>":(I)V
9: astore_1
10: aload_1
11: getfield #4 // Field capacity:I
14: istore_2
15: aload_1
16: getfield #3 // Field buffer:[Ljava/lang/Object;
19: arraylength
20: istore_3
21: return
Мы видим, что обе имеют команду getfield, однако array.length имеет дополнительную команду arraylength
Глядя на спецификацию jvm для arraylength
instructionIsTypeSafe(arraylength, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
nth1OperandStackIs(1, StackFrame, ArrayType),
arrayComponentType(ArrayType, _),
validTypeTransition(Environment, [top], int, StackFrame, NextStackFrame),
exceptionStackFrame(StackFrame, ExceptionStackFrame).
nth1OperandStackIs. Эта команда проверяет, что входящий является ссылочным типом и ссылается на массив. Если ссылка массива равна NULL, генерирует исключение NullPointerException
arrayComponentType. Проверьте тип элементов. Тип компонента массива X - это X
validTypeTransition - правила проверки типов
Таким образом, длина вызова в массиве имеет дополнительную длину arrail.
Очень интересно узнать больше об этом вопросе.
Ответ 4
Я сомневаюсь, что это окажет положительное влияние на производительность. Например, это не поможет устранить связанные проверки в Hotspot. Еще хуже: он может быть быстрее в одной JVM, но, возможно, в следующей версии он болит. Java продолжает получать дополнительные оптимизации, а проверки границ границ - одна вещь, которую они стараются оптимизировать...
Я считаю, что это может быть проблемой перезаписи реального кода очереди, чтобы создать более простой пример. Потому что в реальной очереди вам нужно будет позаботиться об использованной емкости, а иногда вы хотите разрешить верхнюю границу емкости (блокировать производителей, когда потребители не могут идти в ногу). Если у вас есть такой код (с setCapacity/getCapacity и не конечная емкость), и упростите его, удалив логику изменения размера и завершая хранение резервной копии, это то, что вы можете в итоге.