Чи функціональне програмування швидше в багатопотоковому тому, що я пишу речі по-різному або тому, що вони складаються по-різному?


63

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

Наприклад, я написав програму на Java, яка реалізує певний протокол. У цьому протоколі обидві сторони надсилають та отримують одна одній тисячі повідомлень, вони шифрують ці повідомлення та повторно їх повторно (і отримують). Як і очікувалося, багатопотокове читання є ключовим при роботі в масштабах тисяч. У цій програмі блокування не бере участь .

Якщо я напишу ту саму програму в Scala (яка використовує JVM), чи буде ця реалізація швидшою? Якщо так, то чому? Це через стиль письма? Якщо це через стилю письма, тепер, коли Java включає в себе лямбда - вираження, не міг я досягти тих же результатів з допомогою Java з лямбда? Або це швидше, тому що Скала складе речі по-іншому?


64
Функціональне програмування Afaik не робить багатопотоковість швидшою. Це робить багатопотоковість легшою у виконанні та безпечнішою, оскільки є деякі особливості функціонального програмування, такі як незмінність та функції, що не мають побічних ефектів, які допомагають у цьому плані.
Пітер Б

7
Зауважте, що 1) краще насправді не визначено 2) це, безумовно, не визначається як просто "швидше". Мова X, який вимагає в мільярд разів перевищує розмір коду для 0,1% підсилення продуктивності відносно Y, не кращий за Y для будь-якого розумного визначення кращого.
Бакуріу

2
Ви хотіли запитати про "функціональне програмування" або "програми, написані у функціональному стилі"? Часто швидше програмування не дає більш швидкої програми.
Ben Voigt

1
Не забувайте, що завжди є GC, який повинен працювати у фоновому режимі та йти в ногу з вашими вимогами щодо розподілу ... і я не впевнений, що це багатопотокованість ...
Mehrdad

4
Найпростіша відповідь тут: функціональне програмування дозволяє писати програми, які б розглядали менше проблемних умов гонки, однак це не означає, що програми, написані в обов'язковому порядку, будуть повільнішими.
Dawid Pura

Відповіді:


97

Причина, по якій люди кажуть, що функціональні мови краще для паралельної обробки, пов'язана з тим, що вони зазвичай уникають змінних станів. Стан, що змінюється, є "корінням усього зла" в контексті паралельної обробки; вони дуже легко натрапляють на перегони, коли вони поділяються між одночасними процесами. Вирішення умов перегонів тоді включає механізми блокування та синхронізації, як ви вже згадували, які спричиняють накладні витрати часу, оскільки процеси чекають одне одного на використання спільного ресурсу та більшу складність проектування, оскільки всі ці концепції мають тенденцію глибоко вкладені в такі програми.

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

Однак це все випадково. Якщо ваше рішення в Java також уникає змінного стану (спеціально поділяється між потоками), перетворення його на функціональну мову на зразок Scala або Clojure не принесе жодних переваг з точки зору одночасної ефективності, оскільки оригінальне рішення вже не має накладних витрат. механізми блокування та синхронізації.

TL; DR: Якщо рішення в Scala є більш ефективним при паралельній обробці, ніж одне в Java, це не через спосіб компіляції або запуску коду через JVM, а замість того, що рішення Java поділяє стан змін, що змінюється між потоками, або спричиняючи умови перегонів, або додаючи накладні витрати синхронізації, щоб уникнути їх.


2
Якщо лише одна нитка змінює фрагмент даних; особливого догляду не потрібно. Лише тоді, коли декілька потоків можуть змінювати одні й ті самі дані, вам потрібен якийсь особливий догляд (синхронізація, транзакційна пам'ять, блокування тощо). Прикладом цього є стек потоку, який постійно мутується функціональним кодом, але не змінюється кількома потоками.
Брендан

31
Якщо один потік мутує дані, а інші читають, то достатньо, що вам доведеться почати "особливо обережно".
Пітер Грін

10
@Brendan: Ні, якщо один потік змінює дані, а інші потоки читають з цих самих даних, то у вас є умова перегону. Особливий догляд потрібен, навіть якщо змінюється лише одна нитка.
Cornstalks

3
Стан, що змінюється, - це "корінь усього зла" в контексті паралельної обробки => якщо ви ще не дивилися на Руста, раджу поглянути на це. Цьому вдається дозволити змінити ефективність дуже ефективно, усвідомлюючи, що справжнє питання є змінним, змішаним із згладжуванням: якщо у вас є лише псевдонім або маєте лише можливість зміни, проблеми не виникає.
Матьє М.

2
@MatthieuM. Правильно, дякую! Я редагував, щоб висловити речі чіткіше у своїй відповіді. Стан, що змінюється, є лише «корінням усього зла», коли він ділиться між одночасними процесами - чогось Раст уникає зі своїми механізмами контролю власності.
МішельГенріх

8

Сортування обох. Це швидше, тому що простіше написати код таким чином, що його легше скласти швидше. Переміщення швидкостей не обов'язково отримаєш різницю швидкості, але якби ти почав з функціональної мови, ти, певно, міг би зробити багатопотоковість із значно меншими зусиллями програміста . В той же самий спосіб програмісту набагато простіше робити помилки в нарізанні, які коштують швидкості в обов'язковому порядку, і набагато складніше помітити ці помилки.

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


7

Як правило, функціональне програмування не передбачає швидших програм. Це полегшує паралельне та паралельне програмування. Для цього є два основні ключі:

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

Одним з відмінних прикладів пункту №2 є те, що в Хаскеллі ми чітко відрізняємо детермінований паралелізм від недетермінованої одночасності . Немає кращого пояснення, ніж цитувати чудову книгу Саймона Марлоу Паралельне та паралельне програмування в Хаскеллі (цитати з розділу 1 ):

Паралельна програма є один , який використовує безліч обчислювальних апаратних засобів (наприклад, кілька процесорних ядер) для виконання обчислень більш швидко. Мета - досягти відповіді раніше, делегуючи різні частини обчислень різним процесорам, які виконують одночасно.

Навпаки, паралельність - це технологія програмування програми, в якій є кілька потоків управління. Концептуально нитки управління виконуються "одночасно"; тобто користувач бачить їх ефекти переплетеними. Незалежно від того, чи вони насправді виконують одночасно чи ні, це детальна інформація про реалізацію; паралельна програма може виконуватись на одному процесорі через перемежоване виконання або на декількох фізичних процесорах.

На додаток до цього, Марлоу згадує також вимір детермінізму :

Пов'язане розмежування між детермінованими та недетермінованими моделями програмування. Модель детермінованого програмування - це така модель, в якій кожна програма може дати лише один результат, тоді як модель недетермінованого програмування допускає програми, які можуть мати різні результати, залежно від певного аспекту виконання. Моделі одночасного програмування обов'язково не детерміновані, оскільки вони повинні взаємодіяти із зовнішніми агентами, які викликають події в непередбачувані часи. Однак недетермінізм має деякі помітні недоліки: Програми стають значно складнішими для перевірки та обґрунтування.

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

У Хаскеллі паралелізм та паралельність функціонують навколо цих понять. Зокрема, те, що інші мови об'єднуються в один набір функцій, Haskell розбивається на дві:

  • Детерміновані особливості та бібліотеки для паралелізму .
  • Недетерміновані функції та бібліотеки для одночасності .

Якщо ви просто намагаєтеся прискорити чистий, детермінований обчислення, детермінований паралелізм часто полегшує справи. Часто ви просто робите щось подібне:

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

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

Моя програма швидша, ніж якби я перейшов із звичайними багатопотоковими утилітами на основі блокування? Я дуже сумніваюся в цьому. Акуратна річ у моєму випадку отримала стільки ударів від такого маленького долара - мій код, мабуть, дуже неоптимальний, але, оскільки це так просто паралелізувати, я отримав велику швидкість роботи з набагато меншими зусиллями, ніж правильне профілювання та оптимізація, і немає ризику перегонів. І це, я б сказав, головний спосіб функціонального програмування дозволяє писати "швидші" програми.


2

У Haskell модифікація буквально неможлива без отримання спеціальних змінних змінних через бібліотеку модифікацій. Натомість функції створюють необхідні їм змінні одночасно з їх значеннями (які обчислюються ліниво) та сміттям, що збираються, коли вони більше не потрібні.

Навіть коли вам потрібні змінні змінні, ви можете отримати їх, використовуючи просто і разом із немодифікованими змінними. (Ще одна приємна річ у haskell - це STM, який замінює блокування атомними операціями, але я не впевнений, чи це лише для функціонального програмування чи ні.) Зазвичай для покращення речей потрібно зробити паралельно лише одну частину програми. продуктивність.

Це робить паралелізм у Haskell багато часу дуже простим, і насправді ведуться зусилля, щоб зробити його автоматичним. Для простого коду можна навіть розділити паралелізм і логіку.

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

Подальше читання
Паралелізм у Хаскеллі (HaskellWiki)
Паралельне та багатоядерне програмування в паралельному та паралельному програмуванні "Хаскелл у реальному світі"
в Хаскелл Саймоном Марлоу


7
grep java this_post. grep scala this_postі grep jvm this_postповернення результатів не буде :)
Андрес Ф.

4
Питання неясне. У заголовку та першому абзаці він запитує про функціональне програмування взагалі , у другому та третьому абзаці він запитує про Java та Scala зокрема . Це прикро, тим більше, що однією з основних переваг Scala є саме той факт, що вона не є (просто) функціональною мовою. Мартін Одерський називає це "постфункціональним", інші називають його "об'єктно-функціональним". Існує два різних визначення терміна "функціональне програмування". Одне - "програмування за допомогою першокласних процедур" (оригінальне визначення, яке застосовується до LISP), а інше -
Jörg W Mittag

2
"програмування з референтно прозорими, чистими, вільними від побічних функцій та незмінними постійними даними" (набагато суворіше, а також новіше тлумачення). Ця відповідь стосується другої інтерпретації, яка має сенс, оскільки а) перша інтерпретація абсолютно не пов'язана з паралелізмом і одночасністю; б) перша інтерпретація стала в основному безглуздою, оскільки, за винятком С майже кожної мови, навіть у скромно широкому застосуванні сьогодні є першокласні процедури (включаючи Java); в) ОП запитує про різницю між Явою та Scala, але немає…
Jörg W Mittag

2
між двома щодо визначення №1, лише визначення №2.
Йорг W Міттаг

Річ оцінювання не зовсім вірна, як написано тут; За замовчуванням час виконання взагалі не використовує багатопотоковість, а IIRC, навіть якщо ви ввімкнули багатопотокове читання, вам все одно доведеться розповідати час виконання, що він повинен паралельно оцінювати.
Кубічний
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.