Детали реализации имеют значение или direct ByteBuffer vs heap ByteBuffer

Dec 12, 2011 12:02


При написании кода для максимальной производительности особое внимание необходимо уделять используемым библиотекам, даже если эти библиотеки являются частью вашего языка программирования. Необходимо подробно изучать не только контракты, описанные в их API, но и детали их реализации. Для высокопроизводительного кода детали реализации играют огромное значение.
В предыдущей записи про оптимизацию кода, я сравнивал класс ByteBuffer с int[]. Для этого сравнения я использовал прямой буфер (direct buffer), получаемый вызовом метода allocateDirect. Это тот самый буфер, который позволяет вынести данные за пределы кучи в языке Java. А что будет если заменить его на буфер в куче (heap buffer), получаемый вызовом метода allocate?
Проверить это не сложно. Достаточно заменить в одной строчке кода allocateDirect на allocate и произвести измерения. Результаты одного из прогонов моей простенькой тестовой программы, в сравнении с предыдущими результатами для прямого буфера, выглядят так (в таблице показано время в наносекундах, затрачиваемое в расчете на одну итерацию цикла сложения целых чисел из буфера):
РазмерViaByteBuffer2 (direct)ViaByteBuffer3 (heap) 1 0000.779.61 10 0000.779.60 100 0000.869.55 1 000 0000.919.68 10 000 0000.949.76
Скорость работы упала более чем в 10 раз. Чем это можно объяснить? Объяснение легко и просто увидеть, если посмотреть на детали реализации класса ByteBuffer. Внутри методов allocateDirect и allocate создаются конкретные реализации - DirectByteBuffer и HeapByteBuffer соответственно. Посмотрим на реализацию метода getInt в классе DirectByteBuffer:
public int getInt(int i) { return getInt(ix(checkIndex(i, (1 << 2)))); } private int getInt(long a) { if (unaligned) { int x = unsafe.getInt(a); return (nativeByteOrder ? x : Bits.swap(x)); } return Bits.getInt(a, bigEndian); } private long ix(int i) { return address + (i << 0); } // В классе Buffer final int checkIndex(int i, int nb) { if ((i < 0) || (nb > limit - i)) throw new IndexOutOfBoundsException(); return i; }
Видим достаточно прямолинейный код, который проверяет границы, вычисляет адрес, и, при естественном порядке байт и возможности платформы работать с целыми числами независимо от выравнивания, достает целое число напрямую через unsafe.getInt - этот вызов является встроенным (intrinsic) в HotSpot и транслируется в соответствующую инструкцию процессора.
А что происходит в классе HeapByteBuffer? Давайте посмотрим:
public int getInt(int i) { return Bits.getInt(this, ix(checkIndex(i, 4)), bigEndian); } // В классе Bits static int getInt(ByteBuffer bb, int bi, boolean bigEndian) { return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ; } static int getIntL(ByteBuffer bb, int bi) { return makeInt(bb._get(bi + 3), bb._get(bi + 2), bb._get(bi + 1), bb._get(bi )); } static private int makeInt(byte b3, byte b2, byte b1, byte b0) { return (((b3 ) << 24) | ((b2 & 0xff) << 16) | ((b1 & 0xff) << 8) | ((b0 & 0xff) )); } // Снова в классе HeapByteBuffer byte _get(int i) { return hb[i]; }
Даже не вооруженным глазом видно что здесь кода и производимых им вычислений намного больше. После такой же проверки границ как и раньше, каждый их 4-х байтов достается по одному и, путем сдвигов и логических операций, превращается в целое число.
Можно ли из этого сделать вывод, что прямой буфер работает быстрей, чем буфер в куче? Конечно нет! Мы всего лишь увидели, что метод getInt в прямом буфере работает быстрей, однако это ничего не говорит о скорости работы других операций. При написания высокопроизводительного кода, важны детали реализации именно тех методов, которые вы используете, и конкретный способ их применения для решения вашей задачи.
UPDATE: А еще имеет значение что и как замеряется. Подробности в заметке " О замерах времени работы кода или знай что замеряешь часть первая"

programming, performance, bytebuffer, java

Previous post Next post
Up