Pull to refresh

Comments 18

неблокируемость, что означает спо­соб­ность к параллельному выполнению операций ввода-вывода и вычислений


Далеко не Java-специалист, но imho неблокируемость не означает параллельность, скорее асинхронность.
Конечно, асинхронность. Режим тестирования по умолчанию в утилите NIOBench так и называется Asynchronous (см. скриншот).
Между этими понятиями есть причинно-следственная связь: асинхронное выполнение в данном контексте подразумевает, что некоторая программа (назовем ее главной), инициировала выполнение некоторой операции (периферией) и не дожидаясь ее завершения, параллельно может выполнять другие действия.
Не далее как недавно коллега решил с лёгкой руки заменить запись на диск (последовательная запись файла) с Classic IO на NIO — и как результат понижение произодительности

Ради небольшого теста — пишем 2Гб на диск
Classic IO (буфферизированная запись BufferedOutputsteam 1M)

/tmp/test-classicio-3968394580096440112.tmp took 7994.238 ms

Mapped file of FileChannel
/tmp/test-channel-3465783895111396195.tmp took 16993.815 ms

1M Direct ByteBuffer с последующим копированием в FileChannel
/tmp/test-channel-bb-3779212028599883329.tmp took 21364.169 ms

1M Heap ByteBuffer с последующим копированием в FileChannel
/tmp/test-channel-heap-bb-3642623955892374473.tmp took 23544.936 ms


Так, что я бы не стал утверждать, что NIO всегда быстрее и лучше, должно быть есть определенные сценарии, когда он может быть лучше.
От сценария и контекста действительно зависит очень много. Разрабатывая бенчмарки, мы стремились минимизировать зависимость результатов от характеристик Java-машины, чтобы тестировать накопители, а не Java-машину, и не центральный процессор, от производительности которого зависит производительность Java-машины.

Объекты фреймворка NIO, в частности прямые (нативные) буферы оптимальны для этой цели.

В то же время, если существующий контекст Вашего приложения ориентирован на потоки, то модификация с IO на NIO «вырванного из контекста» фрагмента может ухудшить производительность.

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

С точки зрения использования отложенной записи, прочие условия были равными в Ваших опытах? Ваши примеры для NIO не использовали атрибут синхронизации DSYNC?

У нас так (для асинхронного режима, без атрибута DSYNC):

// декларация переменных для каналов
private static FileChannel in[], out[];


// создание канала для файла-источника
in[i] = FileChannel.open( srcPaths[i], CREATE, APPEND );


// создание канала для файла-получателя
out[i] = FileChannel.open( dstPaths[i], CREATE, WRITE );


// пример копирования
in[i].transferTo( 0, in[i].size(), out[i] );
Попробуйте записывать 10-20 файлов, а затем вывести среднее и медиану от полученных результатов. Здесь важно значение в установившемся режиме, в то время как результат нескольких первых файлов может быть экстремальным из-за отложенной записи.

У Вас основные потери времени могут быть связаны с программными циклами внутри измеряемых интервалов, между двумя вызовами метода System.nanoTime();

Например:
for (int i = 0; i < (size / bs.length); i++)

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

Понимаем, конечно, что у нас бенчмарки, поэтому мы стремимся максимально изолировать дисковые операции от влияния контекста. А у Вас конкретная задача по обработке данных, которую надо оптимизировать комплексно.
к чему мне медиана и среднее, если файл пишется один раз? и как бы если за 2 гб оно не выдало макс… это что-то уже не то
Если пишется один файл размером 2 гигабайта, на платформе с 8-16 гигабайт памяти, то искажение, связанное с отложенной записью может иметь место. Рекомендация попробовать 10-20 таких файлов связана с этим фактом, а не только для того, чтобы получить медиану и среднее. Статистика здесь не самоцель.

Но если Ваша задача не бенчмарки диска, а оптимизация конкретной процедуры однократного сохранения такого файла, то конечно, тут другие критерии.
В фреймворке NIO существует еще один метод записи файла, он отсутствует в Вашем примере (MappedTest), и мы также пока его не применяем в NIOBench. Объект AsynchronousFileChannel позволяет использовать явную асинхронность в одном потоке, не прибегая к запуску параллельных потоков.

1) Создаем объект, асинхронный канал
AsynchronousFileChannel writeChannel = AsynchronousFileChannel.open( path, CREATE, WRITE );

2) Создаем объект Future для ожидания события завершения и запускаем операцию.
Future operation = writeChannel.write( buffer, filePosition );

3) После запуска операции (пункт 2) поток освобождается, завершение операции можно проверить методом
operation.isDone();

Ниже (консольное приложение) он отдельно измеряет время передачи задания и время, потраченное на его выполнение. Подразумевается, что каталог C:\TMP существует и созданные файлы не удаляются автоматически.

Нет причин, чтобы такой метод давал более высокую скорость записи, чем другие, но его польза в другом, после выдачи задания (пункт 2) и до завершения его обработки (пункт 3) поток не занят.

package asyncexample1;

import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.concurrent.Future;
import static java.nio.file.StandardOpenOption.*;

public class AsyncExample1 {

private static long startSync, stopSync, stopAsync, filePosition;
private static String writePath = «C:\\tmp\\tmp1.txt»;
private static int bufferSize = 1024*1024;

public static void main(String[] args)
{

//---------- Initializing parameters —
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
filePosition = 0;
startSync = stopSync = stopAsync = 0;

//---------- Initializing buffer —

int n = buffer.limit();
byte[] data = new byte[n];
for ( int i=0; i<n; i++ ) { data[i] = '*'; }
buffer.put(data);
buffer.flip();

//---------- Create file channel — // Note this operation is outside of time measurement interval

Path path = Paths.get(writePath);
try ( AsynchronousFileChannel writeChannel =
AsynchronousFileChannel.open( path, CREATE, WRITE ); )
{

//---------- Send write request, first interval —
startSync = System.nanoTime();
Future operation = writeChannel.write( buffer, filePosition );
stopSync = System.nanoTime();

//---------- Wait for request execution, second interval — while( !operation.isDone() );
stopAsync = System.nanoTime();

//---------- Exception handling —

} catch (Exception e) { System.out.println( «Error: » + e ); }

//---------- Visual timings results —
printInterval( startSync, stopSync, «Send task» );
printInterval( stopSync, stopAsync, «Execute task»);

}

//---------- Helper method for output results to console — // t1 = start interval time moment, nanoseconds
// t2 = end interval time moment, nanoseconds
// s1 = parameter name string for print

private static void printInterval( long t1, long t2, String s1 )
{
double x = t2-t1;
String s2 = «error, »;
if (x<0) { s2 = s2 + «negative time interval»; }
if (x==0) { s2 = s2 + «too small time»; }
if (x>0)
{
x /= 1000000;
s2 = String.format("%.6f ms", x);
}
System.out.println( s1 + " = " + s2 );
}

//---------- End of main class —
}

При верстке Хабра изменила типа кавычек и не позволяет править коммент.
Хм… а там файловая подсистема ОС не вмешалась, случаем? Через что идёт реализация каналов? (Классическое IO, насколько помню, работает напрямую с файловой подсистемой ОС, которая в свой кеш может сожрать до полугига-гига сохраняемых файлов… и таким образом выдать некорректные результаты бенчмаркинга)
была такая мысль — пробовали разные размеры — и 50Мб, и 100Мб, и 500Мб, и разные ОС (windows / linux) — результат в целом один и тот же
У нас, кстати, в процессе тестирование при прочих равных условиях в среде Windows и под Linux Ubuntu 16.04 LTS.
Класс AsynchronousFileChannel позволяет разделить операцию ввода-вывода на синхронную часть (в нашем примере, выдачу задания дисковой подсистеме) и асинхронную часть (выполнение дисковой операции). Такое разделение потенциально дает Java-машине выполнить асинхронную часть в фоновом режиме и максимально освободить ресурсы главного процесса, инициировавшего дисковую операцию. Ключевое слово «потенциально», гарантии параллелизма нет, по причинам, зависящим как от JVM, так и ОС.

При маленьких файлах (около 1MB) эффект нивелируется так как время выполнения задания сравнимо с временем выдачи задания, здесь и в синхронном режиме долго ждать не придется.

При больших файлах, размер которых сравним с размером свободного ОЗУ, отложенная запись может не работать из-за переполнения дискового кэш. Оптимальная середина может зависеть от платформы, стратегия выбора оптимальных размеров блоков должна быть адаптивной.

На картинке отладка примера в среде NetBeans, на виртуальной машине Oracle. Файл 10MB, выдача задания заняла около 14 миллисекунд, выполнение (без учета выдачи) около 38 миллисекунд. Здесь сэкономлено 38 миллисекунд процессорного времени.

Sign up to leave a comment.

Articles