NIO: між Сциллою і Харибдою?

Одним з широко висвітлюватимуться властивостей фреймворку java.NIO є неблокируемость, що означає здатність до паралельного виконання операцій вводу-виводу і обчислень. Якщо додаток, запросившее читання файлу, має обчислювальну задачу, яку можна обробити до отримання даних з файлу, то стає можливим одночасне виконання цих операцій. У разі відкладеного запису, можливостей для паралелізму ще більше, так як при записі, на відміну від читання, додаток не очікує надходження даних.

Примітка. Хотілося б назвати цю статтю «NIO: продуктивність або і сумісність?», але в силу відомих обмежень доводиться цитувати імена цих стародавніх грецьких старух.

Фактором успіху тут є апаратна підтримка. Контролери mass storage пристроїв, такі як SATA AHCI (Advanced Host Controller Interface) і NVMe (Non-Volatile Memory Interface for PCI Express) здатні обробляти досить довгі послідовності операцій вводу-виводу та переміщувати дані між оперативною пам'яттю і накопичувачем в режимі bus-master, без участі програми, виконуваної центральним процесором.

Список команд, що формується драйвера AHCI в оперативній пам'яті і апаратно інтерпретується в microsoft контролером
Рис.1 Список команд, що формується драйвера AHCI в оперативній пам'яті і апаратно інтерпретується в microsoft контролером, може містити до 32 дескрипторів операцій вводу-виводу. Ілюстрація з документа AHCI Specification

Суперечності та вирішення
Тут ми підходимо до ще одній, не менш очевидною, але при цьому дуже важливою характеристикою фреймворку NIO, в основі якого два взаємно-суперечливих критерію:

  1. З одного боку, Java, як мова крос-платформного програмування, абстрагирован від апаратних характеристик обчислювальної системи, архітектури і навіть розрядності центрального процесора. Сумісність вимагає набору абстракцій, надійно відокремлюють прикладного програміста від низькорівневих подробиць. Досить згадати відсутність покажчиків в Java.
  2. З іншого боку, продуктивність вимагає детальної оптимізації коду, його адаптації під особливості конкретної виконує середовища і тип застосовуваного апаратного забезпечення.


Java Native Interface
Одним з альтернативних рішень є пару Java-класів та бібліотек, написаних на C або асемблері. Тут не можна не згадати нативні класи, що реалізують інтерфейс JNI (Java Native Interface), заснований на класичних конвенціях виклику, стандартизуемых для кожної операційної системи і доповнюються механізмом, що забезпечує взаємодію JVM і нативного коду. До речі, оволодівши технологією JNI, можна отримати доступ до вказівників, відсутність яких в Java, іноді доставляє незручність.

У той же час, застосування такого радикального методу як інтеграція нативного коду, нівелює крос-платформні переваги Java, різко підвищує трудомісткість розробки додатків і ймовірність помилок. Підтримка декількох платформ, для JNI-рішення, що неминуче означатиме fork-конструкції з необхідністю написання та супроводу набору бібліотек, кількість яких дорівнює кількості підтримуваних систем.

Треба визнати, є ситуації, коли застосування JNI необхідно. Наприклад, підтримка деяких спеціальних пристроїв, таких як апаратний генератор випадкових чисел.

NIO
Як виявилося, високопродуктивний код можна розробити і на Java, якщо оптимально спроектувати систему абстракцій, инкапсулирующих апаратні ресурси платформи.

Розглянемо операцію читання файлу з диска. Очевидно наявність двох учасників процесу: джерело даних накопичувач або файл) і одержувач (буфер в оперативній пам'яті). Все сказане нижче, справедливо і для запису файлу на диск, що відрізняється тільки напрямок передачі інформації, джерело і одержувач міняються місцями.

Для користувацьких додатків, накопичувач або файл представлений функціями ОС API дискового введення-виведення. Не будемо розглядати можливість прямого програмування регістрів контролера дисків власною програмою, спустилася в минуле з часів MS-DOS, з очевидних міркувань сумісності та безпеки.

Буфер, це заданий діапазон пам'яті в адресному просторі додатки. Якщо бути зовсім нудним точним, то в ряді високорівневих платформ, буфер-одержувач може фізично розміщуватися в кеш-пам'яті центрального процесора, але з дотриманням класичних правил кешування, не втрачаючи асоціації з заданим діапазоном адресів ОЗП.

Отже, розглянуті об'єкти апаратної платформи, це:

  • Канал зв'язку з накопичувачем або файлом (ОС API).
  • Діапазон оперативної пам'яті, буфер.
Реалізувавши Java-класи, прямо відповідають двом названим компонентів «об'єктивної реальності», можна отримати високопродуктивне рішення, за рахунок мінімізації кількості допоміжних операцій, що і зробили розробники фреймворку NIO, в основі якого концепція каналів і буферів.

Звичайно, подібний інструмент не може бути реалізований без низькорівневого апаратно-залежного програмування. Ми говоримо лише про те, що застосування цього інструменту позбавляє прикладного програміста від такої необхідності.

Утиліта NIOBench
Утиліта NIOBench, розроблена IC Book Labs і призначена для вимірювання продуктивності mass storage підсистеми, ілюструє сказане, використовуючи канали і буфери при виконанні файлових операцій.

Утиліта NIOBench, вивід результатів вимірювання швидкості читання, запису і копіювання файлів на жорсткому диску
Рис.2 Утиліта NIOBench, вивід результатів вимірювання швидкості читання, запису і копіювання файлів на жорсткому диску ноутбука ASUS N750JK (при обробці даних використовується медіана і середнє арифметичне)

Текст рапорт утиліти NIOBench з детальним протоколюванням результатів
Рис.3 Текстовий рапорт утиліти NIOBench з детальним протоколюванням результатів: очевидно вплив кешування

Методи вводу-виводу, зокрема операції читання, запису і копіювання файлів, що є об'єктом бенчмарок, засновані на таких архітектурних елементах.

  • Об'єкт FileChannel відповідає каналу, створеного на основі читаного або записуваного файлу. Поряд з найпростішими операціями читання і запису, підтримуються методи transferTo(), transferFrom(), джерелом і одержувачем для яких можуть бути об'єкти файлової системи. Така особливість дуже важлива, оскільки дозволяє копіювати вміст файлів одним java-оператором, витончено позбавляючись від зайвих пересилок даних між декількома буферами. Треба зізнатися, що ефективність оптимізації методів копіювання, характерна для фреймворку NIO, зіграла злий жарт у процесі розробки та налагодження бенчмарок: виміряна швидкість копіювання іноді виявлялася вище швидкості запису.

  • Об'єкт Buffer відповідає діапазону оперативної пам'яті. При всій очевидності цього поняття, зазначимо, що для мінімізації кількості транзитних переміщень даних, рекомендується створювати прямий буфер, використовуючи метод ByteBuffer.allocateDirect();
Недотримання цієї рекомендації може призвести до зниження продуктивності, обумовленого необхідністю перетворення Java-абстракцій в нативні об'єкти, які передаються на обробку функцій ОС API. Інтерфейс JNI також використовується в проекті NIOBench, для підключення бібліотек підтримки апаратного генератора випадкових чисел на основі процесорної інструкції RDRAND (стаття про це Java-бенчмарки: випадкові патерни і закономірні результати» в процесі написання).

Резюме
Концепція каналів і буферів, що лежить в основі технології NIO, точно відповідає архітектурі підсистем зберігання даних, основна функціональність якої зводиться до переміщення інформації між оперативною пам'яттю (буфери) і різноманітними накопичувачами (каналами).

Разом з тим, ніякого дива не сталося, і низькорівнева колективна робота, від якої будь-фреймворк звільняє прикладних програмістів, лише переноситься на розробників фреймворку і системних програмістів…

Посилання
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.