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

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

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

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

Рис.1 Список команд, що формується драйвером AHCI в оперативній пам'яті та апаратно інтерпретований контролером, може містити до 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 підсистеми, ілюструє сказане, використовуючи канали і буфери при виконанні файлових операцій.

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

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

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

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

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

Резюме

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

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

Посилання

  • Про конвенції виклику
  • Про mass storage пристрою
  • Вимоги до платформ