ByteBuffer.allocate () против ByteBuffer.allocateDirect ()

146

Кому allocate()или кому allocateDirect(), вот в чем вопрос.

В течение нескольких лет я просто придерживался мысли, что, поскольку DirectByteBuffers являются прямым отображением памяти на уровне ОС, он будет работать быстрее с вызовами get / put, чем HeapByteBuffers. До сих пор я никогда не был заинтересован в том, чтобы узнать точные детали ситуации. Я хочу знать, какой из двух типов ByteBufferработает быстрее и при каких условиях.

РУМЫНИЯ_engineer
источник
Чтобы дать конкретный ответ, нужно конкретно сказать, что вы с ними делаете. Если один всегда был быстрее другого, зачем было два варианта. Возможно, вы сможете подробнее рассказать, почему вы сейчас «действительно заинтересованы в выяснении точных деталей». Кстати: вы читали код, особенно для DirectByteBuffer?
Питер Лоури
Они будут использоваться для чтения и записи в SocketChannelадреса, настроенные для неблокирования. Итак, что касается того, что сказал @bmargulies, DirectByteBuffers будет работать быстрее для каналов.
@Gnarly По крайней мере, в текущей версии моего ответа говорится, что от каналов ожидается выгода.
bmargulies

Ответы:

153

Рон Хитчес в своей превосходной книге Java NIO, кажется, предлагает то, что, по моему мнению, могло бы стать хорошим ответом на ваш вопрос:

Операционные системы выполняют операции ввода-вывода в областях памяти. С точки зрения операционной системы, эти области памяти представляют собой непрерывные последовательности байтов. Поэтому неудивительно, что только байтовые буферы имеют право участвовать в операциях ввода-вывода. Также помните, что операционная система будет напрямую обращаться к адресному пространству процесса, в данном случае процесса JVM, для передачи данных. Это означает, что области памяти, которые являются целями операций ввода-вывода, должны представлять собой непрерывные последовательности байтов. В JVM массив байтов не может храниться в памяти непрерывно, или сборщик мусора может переместить его в любое время. Массивы - это объекты в Java, и способ хранения данных внутри этого объекта может варьироваться от одной реализации JVM к другой.

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

Прямые байтовые буферы обычно являются лучшим выбором для операций ввода-вывода. По своей конструкции они поддерживают наиболее эффективный механизм ввода-вывода, доступный JVM. Непрямые байтовые буферы могут быть переданы в каналы, но это может привести к снижению производительности. Обычно непрямой буфер не может быть целью собственной операции ввода-вывода. Если вы передаете непрямой объект ByteBuffer в канал для записи, канал может неявно выполнять следующие действия при каждом вызове:

  1. Создайте временный прямой объект ByteBuffer.
  2. Скопируйте содержимое непрямого буфера во временный буфер.
  3. Выполните операцию ввода-вывода низкого уровня, используя временный буфер.
  4. Объект временного буфера выходит за пределы области видимости и в конечном итоге собирается сборщиком мусора.

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

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

Компромиссы производительности при использовании прямых и непрямых буферов могут широко варьироваться в зависимости от JVM, операционной системы и дизайна кода. Выделяя память вне кучи, вы можете подвергнуть свое приложение дополнительным воздействиям, о которых JVM не знает. При вводе дополнительных движущихся частей в игру убедитесь, что вы добиваетесь желаемого эффекта. Я рекомендую старый программный принцип: сначала заставьте его работать, а затем сделайте его быстрым. Не беспокойтесь об оптимизации заранее; сконцентрируйтесь в первую очередь на правильности. Реализация JVM может выполнять кэширование буфера или другие оптимизации, которые обеспечат вам необходимую производительность без особых ненужных усилий с вашей стороны.

Эдвин Далорцо
источник
9
Мне не нравится эта цитата, потому что в ней слишком много предположений. Кроме того, JVM определенно не нужно выделять прямой ByteBuffer при выполнении операций ввода-вывода для непрямого буфера ByteBuffer: достаточно выделить последовательность байтов в куче, выполнить ввод-вывод, скопировать байты в ByteBuffer и освободить байты. Эти области можно даже кэшировать. Но выделять для этого объект Java совершенно необязательно. Реальные ответы будут получены только при измерении. В прошлый раз, когда я проводил измерения, значительной разницы не было. Мне пришлось бы повторить тесты, чтобы выявить все конкретные детали.
Роберт Клемме
4
Сомнительно, может ли книга, описывающая NIO (и собственные операции) содержать определенность. В конце концов, разные JVM и операционные системы управляют вещами по-разному, поэтому автора нельзя винить в том, что он не может гарантировать определенное поведение.
Мартин Тускявичюс
@RobertKlemme, +1, мы все ненавидим догадки, однако, возможно, невозможно измерить производительность для всех основных операционных систем, так как основных операционных систем слишком много. В другом сообщении была сделана попытка сделать это, но мы видим множество проблем с его тестом, начиная с «результаты сильно колеблются в зависимости от ОС». А что, если есть черная овца, которая делает ужасные вещи вроде копирования буфера при каждом вводе-выводе? Затем из-за этой овцы мы можем быть вынуждены отказаться от написания кода, который мы в противном случае использовали бы, просто чтобы избежать этих худших сценариев.
Pacerier
@RobertKlemme Я согласен. Здесь слишком много предположений. Например, JVM вряд ли будет распределять массивы байтов редко.
Маркиз Лорн,
@Edwin Dalorzo: Зачем нам нужен такой байтовый буфер в реальном мире? Они придуманы как средство разделения памяти между процессами? Скажем, например, JVM запускает процесс, и это будет другой процесс, который работает на уровне сети или канала передачи данных, который отвечает за передачу данных. Эти байтовые буферы выделяются для совместного использования памяти между этими процессами? Пожалуйста, поправьте меня, если я ошибаюсь ..
Том Тейлор,
25

Там нет никаких оснований ожидать , прямые буфера быстрее для доступа внутри виртуальной машины. Их преимущество проявляется в том, что вы передаете их в машинный код, например, в код всех типов каналов.

bmargulies
источник
На самом деле. Например, когда необходимо выполнить ввод-вывод в Scala / Java и вызвать встроенные библиотеки Python / собственные библиотеки с большими данными в памяти для алгоритмической обработки или передать данные непосредственно в графический процессор в Tensorflow.
SemanticBeeng
22

поскольку DirectByteBuffers - это прямое отображение памяти на уровне ОС

Это не так. Это просто обычная память процесса приложения, но она не подлежит перемещению во время Java GC, что значительно упрощает работу на уровне JNI. То, что вы описываете, применимо к MappedByteBuffer.

что он будет работать быстрее с вызовами get / put

Вывод не следует из посылки; посылка ложная; и вывод тоже ложный. Они работают быстрее, как только вы попадаете внутрь уровня JNI, и если вы читаете и записываете с того же уровня, DirectByteBufferони намного быстрее, потому что данные никогда не должны пересекать границу JNI.

Маркиз Лорн
источник
7
Это хороший и важный момент: на пути ввода-вывода вы должны в какой-то момент пересечь границу Java - JNI . Прямые и непрямые байтовые буферы перемещают только границу: с прямым буфером все операции размещения из области Java должны пересекаться, тогда как с непрямым буфером все операции ввода-вывода должны пересекаться. Что быстрее, зависит от приложения.
Роберт Клемме
@RobertKlemme Ваше резюме неверно. Со всеми буферами любые данные, поступающие на Java и из нее, должны пересекать границу JNI. Суть прямых буферов в том, что если вы просто копируете данные из одного канала в другой, например, загружаете файл, вам вообще не нужно загружать его в Java, что намного быстрее.
Marquis of Lorne
где именно мое резюме неверно? А с чего "резюме" начать? Я прямо говорил о «операциях размещения из страны Java». Если вы копируете данные только между каналами (т.е. никогда не имеете дело с данными в Java), это, конечно, другая история.
Роберт Клемме
@RobertKlemme Ваше утверждение, что «с прямым буфером [только] все операции put из страны Java должны пересекаться», неверно. И получает, и ставит крест.
Marquis of Lorne
EJP, вы, по-видимому, все еще упускаете из виду предполагаемое различие, которое проводил @RobertKlemme, решив использовать слова «операции размещения» в одной фразе и используя слова «операции ввода-вывода» в противопоставленной фразе предложения. В последней фразе его намерение состояло в том, чтобы сослаться на операции между буфером и каким-либо устройством, предоставляемым ОС.
Нака
19

Лучше всего делать свои собственные измерения. Быстрый ответ, похоже, заключается в том, что отправка из allocateDirect()буфера занимает на 25-75% меньше времени, чем allocate()вариант (тестировался как копирование файла в / dev / null), в зависимости от размера, но само выделение может быть значительно медленнее (даже на в 100 раз).

Источники:

Раф Левиен
источник
Спасибо. Я бы принял ваш ответ, но ищу более подробные сведения о различиях в производительности.