JavaRush /Java блог /Random UA /Паралельні операції над масивами Java 8 - переклад
billybonce
29 рівень
Москва

Паралельні операції над масивами Java 8 - переклад

Стаття з групи Random UA
переклад статті
//Parallel Array Operations in Java 8 //By Eric Bruno, March 25, 2014 //drdobbs.com/jvm/parallel-array-operations-in-java-8/240166287 //Eric Bruno працює у фінансовому секторі та веде блоги для сайту Dr. Dobb's.
Новий реліз Java спрощує паралельну взаємодію з масивами – що призводить до значно покращеної продуктивності з мінімумом написання коду. Зараз Oracle випускає Java SE 8 - що є величезним кроком вперед у плані мови. Одна з важливих деталей цього релізу – покращена concurrency (паралельність), частина якої з'являється у базовому класі java.util.Arrays. У цей клас додані нові методи, які я і описуватиму в цій статті. Деякі з них використовуються в іншій новій фічі JDK8 - в lambda (лямбдах). Але перейдемо до діла.
Arrays.paralellSort()
Багато особливостей parallelSort базується на паралельному алгоритмі сортування злиттям, який рекурсивно поділяє масив на частини, сортує їх, а потім рекомбінує їх одночасно в підсумковий масив. Використовуючи його замість існуючого, послідовного методу Arrays.sort виявляється у покращеній продуктивності та ефективності при сортуванні великих масивів. Наприклад, код нижче використовує послідовне сортування sort() і паралельне parallelSort() для того, щоб відсортувати один і той же масив даних: Для перевірки, я public class ParallelSort { public static void main(String[] args) { ParallelSort mySort = new ParallelSort(); int[] src = null; System.out.println("\nSerial sort:"); src = mySort.getData(); mySort.sortIt(src, false); System.out.println("\nParallel sort:"); src = mySort.getData(); mySort.sortIt(src, true); } public void sortIt(int[] src, boolean parallel) { try { System.out.println("--Array size: " + src.length); long start = System.currentTimeMillis(); if ( parallel == true ) { Arrays.parallelSort(src); } else { Arrays.sort(src); } long end = System.currentTimeMillis(); System.out.println( "--Elapsed sort time: " + (end-start)); } catch ( Exception e ) { e.printStackTrace(); } } private int[] getData() { try { File file = new File("src/parallelsort/myimage.png"); BufferedImage image = ImageIO.read(file); int w = image.getWidth(); int h = image.getHeight(); int[] src = image.getRGB(0, 0, w, h, null, 0, w); int[] data = new int[src.length * 20]; for ( int i = 0; i < 20; i++ ) { System.arraycopy( src, 0, data, i*src.length, src.length); } return data; } catch ( Exception e ) { e.printStackTrace(); } return null; } } завантажив чисті дані із зображення в масив, що зайняло 46,083,360 байт(а у вас буде залежати від зображень які використовуватимете). Метод із послідовним сортуванням зайняв майже 3,000 мілісекунд для того, щоб відсортувати масив на моєму 4-х ядерному ноутбуці, у той час як метод паралельного сортування зайняв максимум близько 700 мілісекунд. Погодьтеся, не часто трапляється, щоб нове оновлення мови покращувало б продуктивність класу в 4 рази.
Arrays.parallelPrefix()
Метод parallelPrefix застосовує задану математичну функцію до елементів масиву в сукупності, обробляючи результати всередині масиву паралельно. Це набагато ефективніше на сучасному багатоядерному залозі, порівняно з послідовними операціями великих масивах. Є багато реалізацій цього методу для різних базових типів операцій над даними (наприклад IntBinaryOperator, DoubleBinaryOperator, LongBinaryOperator тощо), як і для різних типів математичних операторів. Наведу приклад підсумовування з накопиченням на паралельному масиві, що використовує той самий великий масив, як і в попередньому прикладі, який завершується приблизно за 100 мілісекунд на моєму 4-х ядерному ноутбуці. public class MyIntOperator implements IntBinaryOperator { @Override public int applyAsInt(int left, int right) { return left+right; } } public void accumulate() { int[] src = null; // accumulate test System.out.println("\nParallel prefix:"); src = getData(); IntBinaryOperator op = new ParallelSort.MyIntOperator(); long start = System.currentTimeMillis(); Arrays.parallelPrefix(src, new MyIntOperator()); long end = System.currentTimeMillis(); System.out.println("--Elapsed sort time: " + (end-start)); } ... }
Arrays.parallelSetAll()
Новий метод parallelSetAll() створює масив і встановлює кожному елементу масиву значення відповідно до функції, що генерує ці значення, використовуючи паралельність для підвищення ефективності. Цей метод заснований на лямбдах (званих "замикання" (closures) в інших мовах) (і, так, тут помилка автора, бо лямбди і замикання це різні речі) , та які є ще однією новинкою JDK8, яку ми обговоримо у майбутніх статтях. Досить помітити, лямбди, чий синтаксис легко впізнати по оператору ->, що робить операцію над правою частиною після стрілки для всіх переданих йому елементів. У прикладі коду, представленому нижче - дія проводиться для кожного елемента масиві, проіндексованого по i. Array.parallelSetAll() генерує елементи масиву. Наприклад, наступний код заповнює великий масив випадковими integer-значеннями: public void createLargeArray() { Integer[] array = new Integer[1024*1024*4]; // 4M Arrays.parallelSetAll( array, i -> new Integer( new Random().nextInt())); } Для створення складнішого генератора елементів масиву (наприклад, такого, що генерував би значення на основі зчитування з датчиків з реального світу), можна використовувати код близький до наступного: public void createLargeArray() { Integer[] array = new Integer[1024*1024*4]; // 4M Arrays.parallelSetAll( array, i -> new Integer( customGenerator(getNextSensorValue()))); } public int customGenerator(int arg){ return arg + 1; // some fancy formula here... } public int getNextSensorValue() { // Just random for illustration return new Random().nextInt(); } Ми почнемо з getNextSensorValue, який насправді буде запитувати датчик (наприклад, термометр) повернути йому поточне значення. Тут для прикладу генерується випадкове значення. Наступний customGenerator() метод генерує масив елементів з використанням вибраної логіки на основі вибраного вами випадку. Ось невелике доповнення, але для реальних випадків, тут було б щось складніше.
Що таке Spliterator?
Інше доповнення до класу Arrays, яке використовує паралельність та лямбди – це Spliterator, який використовується для ітерації та поділу масиву. Його дія не обмежена лише масивами – він також добре працює і для класів Collection та IO каналів. Spliterator'и працюють на основі автоматичного розбиття масиву на різні частини, а новий Spliterator встановлюється для того, щоб здійснювати операції над цими пов'язаними підмасивами. Його назва складається з Iterator(ітератора), який "поділяє"(splits) його роботу з переміщення-ітерації на частини. Використовуючи наші, ті самі дані, ми можемо зробити окремо ітеровану(splititerated) дію над нашим масивом так: public void spliterate() { System.out.println("\nSpliterate:"); int[] src = getData(); Spliterator spliterator = Arrays.spliterator(src); spliterator.forEachRemaining( n -> action(n) ); } public void action(int value) { System.out.println("value:"+value); // Perform some real work on this data here... } Виконання дій над даними у такий спосіб використовує плюси паралельності. Можна також встановити параметри сплітітератора, такі як мінімальний розмір кожного підмасиву.
Stream - обробка
Нарешті, з масиву (Array), ви можете створювати об'єкт Stream, який дозволяє проводити паралельну обробку на вибірці даних як загалом, узагальненому в послідовність-потік (stream). Різниця між колекцією (Collection) даних і послідовністю-потоком (Stream) з нової JDK8 така, що колекції дозволяють працювати з елементами окремо, коли як послідовність потоку не дозволяє. Наприклад, з використанням колекцій, ви можете додавати елементи, видаляти і вставляти в середину. Послідовність-потік Stream не дозволяє маніпулювати окремими елементами з набору даних, але натомість дозволяє виконувати функції над даними як одним цілим. Ви можете виконувати такі корисні операції, як витягти тільки конкретні значення (ігноруючи повтори) з набору, операції перетворення даних, знаходження мінімумів і максимумів масиву, функцій map-reduce (використовуються при розподілених обчисленнях) та інших математичних операціях. Наступний простий приклад використовує concurrency для паралельної обробки масиву даних та підсумовування елементів. public void streamProcessing() { int[] src = getData(); IntStream stream = Arrays.stream(src); int sum = stream.sum(); System.out.println("\nSum: " + sum); }
Висновок
Java 8 безперечно буде одним з найкорисніших оновлень мови. Паралельні фічі, згадані тут, лямбди, та багато інших розширень, будуть предметом розгляду на нашому сайті в інших оглядах Java 8.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ