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

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

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

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

The Pony Gap

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

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

  • MacBook Pro с двухъядерным процессором Core 2.
  • Дисковод Intel X25M SSD
  • 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);      
    }
}

Ответ 1

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

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

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

Большие распределения в обход TLAB

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

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

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

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

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

-XX:TLABSize= -XX:-ResizeTLAB

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

Параметры диагностической печати

-XX:+PrintTLAB

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

Ответ 2

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

Сначала я немного удивлен, что это не общее знание, но нести его w/me

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

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

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

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

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

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

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

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

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

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

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

Ответ 3

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

Ответ 4

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

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

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