Почему нечетная разница в производительности между ByteBuffer.allocate () и ByteBuffer.allocateDirect () - PullRequest
31 голосов
/ 06 сентября 2010

Я работаю над кодом SocketChannel -to- SocketChannel, который лучше всего подойдет для прямого байтового буфера - долгоживущего и большого (от десятков до сотен мегабайт на соединение). При этом хэшируя точный циклСтруктура с FileChannel s, я провел несколько микропроцессоров по производительности ByteBuffer.allocate() против ByteBuffer.allocateDirect().

Результаты были неожиданными, которые я не могу объяснить.На приведенном ниже графике для реализации переноса ByteBuffer.allocate() имеется очень выраженный обрыв в 256 КБ и 512 КБ - производительность падает на ~ 50%!Также кажется, что для ByteBuffer.allocateDirect() производительность будет меньше.(Серия% усиления помогает визуализировать эти изменения.)

Размер буфера (в байтах) в зависимости от времени (MS)

The Pony Gap

Почему нечетная разница в кривой производительности между ByteBuffer.allocate() и ByteBuffer.allocateDirect()? Что именно происходит за кулисами?

Это очень хорошо, возможно, зависит от оборудования и ОСВот эти подробности:

  • MacBook Pro с двухъядерным процессором Core 2
  • SSD-накопитель Intel X25M
  • OSX 10.6.4

Исходный код, по запросу:

package ch.dietpizza.bench;

import static java.lang.String.format;
import static java.lang.System.out;
import static java.nio.ByteBuffer.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

public class SocketChannelByteBufferExample {
    private static WritableByteChannel target;
    private static ReadableByteChannel source;
    private static ByteBuffer          buffer;

    public static void main(String[] args) throws IOException, InterruptedException {
        long timeDirect;
        long normal;
        out.println("start");

        for (int i = 512; i <= 1024 * 1024 * 64; i *= 2) {
            buffer = allocateDirect(i);
            timeDirect = copyShortest();

            buffer = allocate(i);
            normal = copyShortest();

            out.println(format("%d, %d, %d", i, normal, timeDirect));
        }

        out.println("stop");
    }

    private static long copyShortest() throws IOException, InterruptedException {
        int result = 0;
        for (int i = 0; i < 100; i++) {
            int single = copyOnce();
            result = (i == 0) ? single : Math.min(result, single);
        }
        return result;
    }


    private static int copyOnce() throws IOException, InterruptedException {
        initialize();

        long start = System.currentTimeMillis();

        while (source.read(buffer)!= -1) {    
            buffer.flip();  
            target.write(buffer);
            buffer.clear();  //pos = 0, limit = capacity
        }

        long time = System.currentTimeMillis() - start;

        rest();

        return (int)time;
    }   


    private static void initialize() throws UnknownHostException, IOException {
        InputStream  is = new FileInputStream(new File("/Users/stu/temp/robyn.in"));//315 MB file
        OutputStream os = new FileOutputStream(new File("/dev/null"));

        target = Channels.newChannel(os);
        source = Channels.newChannel(is);
    }

    private static void rest() throws InterruptedException {
        System.gc();
        Thread.sleep(200);      
    }
}

Ответы [ 4 ]

27 голосов
/ 13 июня 2012

Как работает ByteBuffer и почему прямые (байтовые) буферы теперь действительно полезны.

Во-первых, я немного удивлен, что это не общеизвестно, но терпите меня со мной

Прямые байтовые буферы выделяют адрес вне кучи Java.

Это крайне важно: все функции ОС (и родной C) могут использовать этот адрес без блокировки объекта в куче и копирования данных. Краткий пример копирования: для отправки любых данных через Socket.getOutputStream (). Write (byte []) нативный код должен «заблокировать» byte [], скопировать его вне кучи Java, а затем вызвать функцию ОС, например, отправить . Копирование выполняется либо в стеке (для меньшего байта []), либо через malloc / free для больших. DatagramSockets ничем не отличаются, и они также копируют - за исключением того, что они ограничены 64 КБ и размещены в стеке, что может даже убить процесс, если стек потока не достаточно велик или имеет глубокую рекурсию. примечание: блокировка не позволяет JVM / GC перемещать / перераспределять объект вокруг кучи

Итак, с введением NIO идея заключалась в том, чтобы избежать копирования и множества потоковых конвейеров / косвенных указаний. Часто существует 3-4 буферизованных типа потоков, прежде чем данные достигнут своего места назначения. (ууу Польша выравнивает (!) Красивым выстрелом) Внедряя прямые буферы, java может напрямую связываться с нативным кодом C без какой-либо блокировки / копирования. Следовательно, функция sent может взять адрес буфера, добавить позицию, и производительность будет практически такой же, как и у собственного C. Это о прямом буфере.

Основная проблема с прямыми буферами - они дорогостоящи для выделения и дороги для освобождения и довольно громоздки в использовании, ничего подобного байту [].

Непрямой буфер не предлагает истинную сущность, которую делают прямые буферы - то есть прямой мост к нативной / ОС, вместо этого они легковесны и имеют одинаковый API - и даже больше, они могут wrap byte[] и даже их резервный массив доступен для прямого манипулирования - что не любить? Ну, они должны быть скопированы!

Так как же Sun / Oracle обрабатывает непрямые буферы, так как OS / native не может использовать их - ну, наивно. Когда используется непрямой буфер, должна быть создана прямая противоположная часть. Реализация достаточно умна, чтобы использовать ThreadLocal и кэшировать несколько прямых буферов с помощью SoftReference*, чтобы избежать изрядных затрат на создание. Наивная часть возникает при их копировании - она ​​пытается скопировать весь буфер (remaining()) каждый раз.

Теперь представьте: 512 КБ непрямого буфера идет в буфер 64 КБ, буфер сокета не займет больше его размера. Таким образом, в первый раз 512 КБ будут скопированы из непрямого в поток-локальный-прямой, но только 64 КБ будут использованы. В следующий раз будет скопировано 512-64 КБ, но будет использовано только 64 КБ, а в третий раз будет скопировано 512-64 * 2 КБ, но будет использовано только 64 КБ, и так далее ... и это оптимистично, что всегда сокет буфер будет полностью пуст. Таким образом, вы копируете не только n КБ, но и n & times; n & делить; m (n = 512, m = 16 (среднее пространство, оставленное буфером сокета)).

Копирующая часть является общим / абстрактным путем ко всему непрямому буферу, поэтому реализация никогда не знает целевой емкости. Копирование кешей, а что нет, уменьшение пропускной способности памяти и т. Д.

* Примечание по кешированию SoftReference: это зависит от реализации GC и может варьироваться. GC от Sun использует свободную память кучи для определения срока службы SoftRefences, что приводит к некоторому неловкому поведению при их освобождении - приложению необходимо снова выделить ранее кэшированные объекты, то есть большее выделение (прямые байтовые буферы занимают незначительную часть в куче, поэтому по крайней мере, они не влияют на дополнительную очистку кэша, но влияют вместо этого)

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

Этот микро-бенчмарк является в основном тестом пропускной способности памяти, ОС будет иметь файл полностью в кеше, поэтому в основном тестирует memcpy Как только буферы заканчиваются из кэша L2, падение производительности должно быть заметно. Кроме того, выполнение такого эталонного теста приводит к увеличению и накоплению затрат на сбор ГХ. (rest() не будет собирать мягкие ссылки на ByteBuffers)

26 голосов
/ 06 сентября 2010

Буферы локального распределения потоков (TLAB)

Интересно, составляет ли буфер локального выделения потока (TLAB) во время теста около 256 КБ. Использование TLAB оптимизирует выделение из кучи, так что непрямое выделение <= 256K является быстрым. </p>

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

Большие выделения в обход TLAB

Если моя гипотеза о 256 КБ TLAB верна, то информация, приведенная далее в этой статье, предполагает, что, возможно, выделения> 256 КБ для более крупных непрямых буферов обходят TLAB. Эти распределения идут прямо в кучу, требуя синхронизации потоков, что приводит к снижению производительности.

Распределение, которое не может быть сделано из TLAB, не всегда означает, что поток должен получить новый TLAB. В зависимости от размера выделения и неиспользуемого пространства, оставшегося в TLAB, виртуальная машина может принять решение просто выделить из кучи. Это распределение из кучи потребовало бы синхронизации, но также получило бы новый TLAB. Если распределение считалось большим (некоторая значительная часть текущего размера TLAB), распределение всегда выполнялось бы из кучи. Это сокращало потери и изящно обрабатывало много -выделение выше среднего.

Настройка параметров TLAB

Эта гипотеза может быть проверена с использованием информации из более поздней статьи, указывающей, как настроить TLAB и получить диагностическую информацию:

Чтобы поэкспериментировать с определенным размером TLAB, нужны два флага -XX быть установленным, один для определения начального размера и один для отключения изменение размера:

-XX:TLABSize= -XX:-ResizeTLAB

Минимальный размер tlab устанавливается с -XX: MinTLABSize, который по умолчанию 2K байтов. Максимальный размер - максимальный размер целочисленного массива Java, который используется для заполнения нераспределенного часть TLAB при очистке ГХ.

Опции диагностической печати

-XX:+PrintTLAB

Печатает в каждом мусоре одну строку для каждого потока (начинается с "TLAB: gc thread:" без ") и одну итоговую строку.

7 голосов
/ 02 октября 2010

Я подозреваю, что эти колени происходят из-за отключения кеш-памяти. Реализация «непрямого» буфера read () / write () «пропускает кэш» ранее из-за дополнительной копии буфера памяти по сравнению с реализацией «прямого» буфера read () / write ()

0 голосов
/ 06 сентября 2010

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

Некоторые догадки:

  • Возможно, вы достигли максимального количества байтов, которые могут быть прочитаны за раз, таким образом, IOwaits увеличивается или потребление памяти увеличивается без уменьшения циклов.
  • Возможно, вы достигли критического предела памяти или JVM пытается освободить память перед новым распределением. Попробуйте поиграться с параметрами -Xmx и -Xms
  • Возможно, HotSpot не может / не будет оптимизировать, потому что количество вызовов некоторых методов слишком мало.
  • Может быть, существуют условия ОС или оборудования, которые вызывают такую ​​задержку
  • Может быть, реализация JVM просто глючит; -)
...