Чому Хаскелл (GHC) так проклятий?


247

Haskell (з GHCкомпілятором) набагато швидше, ніж ви очікували . Якщо правильно використати, він може наблизитись до мов низького рівня. (Улюблена річ Haskellers - це спробувати потрапити в межах 5% С (або навіть перемогти його, але це означає, що ви використовуєте неефективну програму C, оскільки GHC компілює Haskell на C).) Моє питання: чому?

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

Хоча найдивнішим є функція вищого порядку. Ви б могли подумати, що створення функцій на ходу та перекидання їх зробить програму повільнішою. Але використання функцій вищого порядку фактично робить Haskell швидшим. Дійсно, здається, що для оптимізації коду Haskell вам потрібно зробити його більш елегантним та абстрактним, а не більш машинним. Жодна з вдосконалених функцій Haskell, здається, навіть не впливає на її продуктивність, якщо вони не покращують її.

Вибачте, якщо це звучить безглуздо, але ось моє запитання: Чому Haskell (складений з GHC) такий швидкий, враховуючи його абстрактний характер та відмінності від фізичних машин?

Примітка. Причина, по якій я кажу, що C та інші імперативні мови дещо схожі на машини Тюрінга (але не настільки, наскільки Хаскелл схожий на обчислення Ламбди), полягає в тому, що в імперативній мові у вас є кінцева кількість станів (також номер рядка) разом із стрічкою (таран) таким чином, що стан та поточна стрічка визначають, що робити із стрічкою. Дивіться запис у Вікіпедії " Еквіваленти машин Тюрінга" щодо переходу від машин Тьюрінга до комп'ютерів.


27
"оскільки GHC компілює Haskell на C" - це не так. У GHC є декілька варіантів. Найдавніший (але не за замовчуванням) - це генератор C. Він генерує код Cmm для ІЧ, але це не "компіляція на C", яку ви зазвичай очікували. ( downloads.haskell.org/~ghc/latest/docs/html/users_guide/… )
viraptor

20
Я настійно рекомендую ознайомитись із впровадженням функціональних мов програмування Саймоном Пейтоном Джонсом (основним виконавцем GHC), що відповість на багато ваших питань.
Джо Хілленбранд

94
Чому? 25 років наполегливої ​​праці.
серпень

31
"Хоча на це може бути фактична відповідь, це не буде нічого, окрім як вимагати думок". - Це найгірша можлива причина закрити питання. Тому що це може дати хорошу відповідь, але потенційно це також приверне неякісні. Гидота! У мене так трапляється хороша, історична, фактична відповідь, яка вибудовувалася про академічні дослідження та коли відбувалися певні події. Але я не можу розмістити його, тому що люди переживають, що це питання також може залучати неякісні відповіді. Знову, юк.
sclv

7
@cimmanon Мені знадобиться місяць або кілька публікацій блогу, щоб ознайомитися з основами деталей того, як працює функціональний компілятор. Мені потрібна лише відповідь SO, щоб накреслити широкий контур, як графічну машину можна чітко реалізувати на фондовому обладнання та вказати на відповідні джерела для подальшого читання ...
sclv

Відповіді:


264

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

Перш за все, Хаскелл дуже високого рівня. Це дозволяє компілятору проводити агресивні оптимізації, не порушуючи код.

Подумайте про SQL. Тепер, коли я пишу SELECTзаяву, це може виглядати як імперативний цикл, але це не так . Це може виглядати так, що він перетинає всі рядки в цій таблиці, намагаючись знайти той, що відповідає заданим умовам, але насправді "компілятор" (двигун БД) міг би робити пошук індексу - який має зовсім інші характеристики продуктивності. Але оскільки SQL настільки високого рівня, "компілятор" може підміняти абсолютно різні алгоритми, прозоро застосовувати декілька процесорів або каналів вводу / виводу або цілі сервери тощо.

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

Ще один спосіб поглянути на це може бути ... чому не повинен Haskell бути швидким? Що це робить, щоб зробити це повільним?

Це не інтерпретована мова, як Perl чи JavaScript. Це навіть не така система віртуальної машини, як Java або C #. Він компілюється аж до рідного машинного коду, тому ніяких накладних витрат там немає.

На відміну від мов OO [Java, C #, JavaScript…], Haskell має стирання повного типу [як C, C ++, Pascal ...]. Перевірка всіх типів відбувається лише під час компіляції. Таким чином, не існує перевірки типу виконання часу, щоб також сповільнити вас. (Немає жодної перевірки нульових покажчиків. У цьому випадку, скажімо, Java, JVM повинен перевірити наявність нульових покажчиків і викинути виняток, якщо вам це належить. Haskell не повинен турбуватися з цією перевіркою.)

Ви говорите, що це звучить повільно, "створюючи функції під час руху під час виконання", але якщо ви подивитеся дуже уважно, ви насправді цього не робите. Це може виглядати так, як ти, але цього не робиш. Якщо ви скажете (+5), ну, це важко закодовано у ваш вихідний код. Він не може змінитися під час виконання. Тож насправді це не динамічна функція. Навіть криві функції - це просто збереження параметрів у блок даних. Весь виконуваний код фактично існує під час компіляції; немає інтерпретації часу виконання. (На відміну від деяких інших мов, які мають "функцію eval".)

Подумайте про Паскаля. Він старий, і його ніхто вже не використовує, але ніхто не скаржиться, що Паскаль повільний . Є багато чого, що йому не сподобається, але повільність насправді не одна з них. Haskell насправді не робить так багато, що відрізняється від Pascal, за винятком того, що збирає сміття, а не ручне управління пам’яттю. І незмінні дані дозволяють здійснити декілька оптимізацій для двигуна GC [яке лінива оцінка потім дещо ускладнює].

Я думаю, річ у тому, що Хаскелл виглядає передовим, витонченим та високим рівнем, і всі думають: "О, ух, це справді потужно, це повинно бути дивно повільно! " Але це не так. Або принаймні, це не так, як ви очікували. Так, у нього дивовижна система типу. Але ти знаєш що? Це все відбувається під час компіляції. До часу виконання його вже немає. Так, це дозволяє будувати складні ADT з рядком коду. Але ти знаєш що? ADT - це просто звичайний C unionof structs. Більше нічого.

Справжній вбивця - лінива оцінка. Коли ви правильно зрозумієте суворість / лінь свого коду, ви можете написати тупо швидкий код, який все ще є елегантним і красивим. Але якщо ви помиляєтесь з цим матеріалом, ваша програма йде в тисячі разів повільніше , і це дійсно не очевидно, чому це відбувається.

Наприклад, я написав тривіальну маленьку програму, щоб підрахувати, скільки разів кожен байт з'являється у файлі. Для вхідного файлу розміром 25 Кб програмі знадобилося 20 хвилин, і проковтнув 6 гігабайт оперативної пам’яті! Це абсурд !! Але потім я зрозумів, в чому проблема, додав єдиний вибух, і час запуску знизився до 0,02 секунди .

Тут Хаскелл несподівано йде повільно. І це звичайно потребує певного часу, щоб звикнути. Але з часом стає простіше писати дійсно швидкий код.

Що робить Haskell настільки швидким? Чистота. Статичні типи. Лінь. Але перш за все, будучи достатньо високим рівнем, що компілятор може кардинально змінити реалізацію, не порушуючи очікувань вашого коду.

Але я думаю, це лише моя думка ...


13
@cimmanon Я не думаю, що це суто засноване на думках. Це цікаве запитання, на яке, ймовірно, хотіли відповіді інші люди. Але я думаю, ми побачимо, що думають інші виборці.
Математична

8
@cimmanon - цей пошук містить лише півтори теми, і всі вони мають відношення до аудиторських перевірок. і відповідь, що обнародує, говорить: "Будь ласка, припиніть модерувати речі, які ви не розумієте". Я б припустив, що якщо хтось вважає, що відповідь на це обов'язково занадто широкий, він би здивувався і отримав задоволення від відповіді, оскільки відповідь не надто широкий.
sclv

34
"У, скажімо, Java, JVM повинен перевірити наявність нульових покажчиків і викинути виняток, якщо ви відзначите його." Явна явна перевірка нуля Java (здебільшого) дорожча. Реалізації Java можуть і використовувати переваги віртуальної пам’яті для відображення нульової адреси на відсутній сторінці, тому перенаправлення нульового покажчика ініціює помилку сторінки на рівні процесора, яка Java вловлює та видаляє як виняток високого рівня. Тож більшість нульових перевірок здійснюється блоком картографічної пам'яті в ЦП безкоштовно.
Боан

4
@cimmanon: Можливо, це тому, що користувачі Haskell, здається, є єдиною спільнотою, яка насправді є доброзичливою групою відкритих людей ... яку ви вважаєте «жартом»…, а не спільнотою-собакою, яка править нацистами, видобувати один одного нового при кожному шансі, що вони отримають ... що, здається, те, що ви вважаєте "нормальним".
Evi1M4chine

14
@MathematicalOrchid: чи є у вас копія оригінальної програми, на яку запустили 20 хв? Я думаю, було б досить повчально вивчити, чому це так повільно.
Джордж

79

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

Друга хвиля конструкцій виникла на основі скорочення графіків і відкрила можливість для набагато ефективнішої компіляції. Саймон Пейтон Джонс писав про це дослідження у двох своїх книгах «Впровадження функціональних мов програмування та реалізація функціональних мов: навчальний посібник (перший з розділами Вадлера та Хенкока, а другий написаний з Девідом Лестером). (Леннарт Авгуссон також повідомив мені, що однією з основних мотивацій колишньої книги було описування способу, коли його компілятор LML, який не був широко коментуваний, здійснив її складання).

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

Це описано в більш пізньому документі, в якому викладаються основи того, як GHC все ще працює сьогодні (хоча модульно багато різноманітних налаштувань): "Впровадження лінивих функціональних мов на фондовому обладнанні: безхребетна беззмістовна машина". . Поточна модель виконання GHC більш детально задокументована у GHC Wiki .

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

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

На додаток до цього існує багато інших оптимізацій, які відкриваються, зокрема, чистотою, оскільки це дозволяє більшу кількість "безпечних" перетворень. Коли і як застосувати ці перетворення, щоб вони покращили і не стали гіршими, це, звичайно, емпіричне питання, і щодо цього та багатьох інших невеликих виборів роки роботи були введені як в теоретичну роботу, так і в практичне тестування. Тож це, звичайно, також відіграє певну роль. Документ, що дає хороший приклад такого роду досліджень, - це " Створення швидкої каррі: натиснути / ввести проти Евала / подати заявки на мови вищого порядку".

Нарешті, слід зазначити, що ця модель все ж вводить накладні витрати через непрямі. Цього можна уникнути у випадках, коли ми знаємо, що "безпечно" робити речі суворо і, таким чином, ухилятися від непрямих графіків. Механізми, які випливають із суворості / вимоги, знову детально задокументовані в GHC Wiki .


2
Та посилання аналізатора попиту вартує ваги золота! Нарешті щось про тему, яка не діє так, ніби це в основному незрозуміла чорна магія. Як я ніколи про це не чув ?? Це має бути пов’язане скрізь, де хтось запитує, як вирішити проблеми з лінню!
Evi1M4chine

@ Evi1M4chine Я не бачу посилання, пов’язаного з аналізатором попиту, можливо, воно якось загублено. Чи може хтось відновити посилання або уточнити посилання? Це звучить досить цікаво.
Cris P

1
@CrisP Я вважаю, що остання посилання - це те, про що йдеться. Він переходить на сторінку в GHC Wiki про аналізатор попиту в GHC.
Серп С

@ Серпентин Кугар, Кріс П: Так, це я мав на увазі.
Evi1M4chine

19

Ну, тут є багато що коментувати. Я спробую відповісти, наскільки зможу.

Якщо правильно використати, він може наблизитись до мов низького рівня.

На моєму досвіді, як правило, в багатьох випадках можливо отримати в два рази виступ "Іржа". Але також є деякі (широкі) випадки використання, коли продуктивність низька порівняно з мовами низького рівня.

або навіть перемогти його, але це означає, що ви використовуєте неефективну програму C, оскільки GHC компілює Haskell на C)

Це не зовсім правильно. Haskell компілюється в C-- (підмножину C), яка потім компілюється через власний генератор коду для збирання. Народний генератор коду зазвичай генерує швидший код, ніж компілятор C, оскільки він може застосувати деякі оптимізації, які звичайний компілятор C не може.

Машинні архітектури, безумовно, є обов'язковими, базуючись на машинах із твердженням, приблизно.

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

Дійсно, у Haskell навіть немає конкретного порядку оцінки.

На насправді, Haskell має неявно визначає порядок оцінки.

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

Вони відповідають у багатьох випадках, якщо у вас є достатньо просунутий компілятор.

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

Haskell компілюється, і тому функції вищого порядку фактично не створюються на льоту.

це здається оптимізувати код Haskell, вам потрібно зробити його більш елегантним та абстрактним, а не більше машинного типу.

Взагалі, зробити код більш "машиноподібним" - це непродуктивний спосіб отримати кращі показники роботи в Haskell. Але зробити це абстрактнішим не завжди є гарною ідеєю. Що є хорошою ідеєю є використання загальних структур і функцій , які були сильно оптимізовані (наприклад, пов'язані списки) даних.

f x = [x]і f = pureце саме те саме в Haskell, наприклад. Хороший компілятор не дав би кращих показників у першому випадку.

Чому Haskell (складений з GHC) настільки швидкий, враховуючи його абстрактний характер та відмінності від фізичних машин?

Коротка відповідь "тому, що вона була покликана зробити саме це". GHC використовує безшпинну безметежну г-машину (STG). Ви можете прочитати документ про це тут (це досить складно). GHC також робить багато інших речей, таких як аналіз строгості та оптимістична оцінка .

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

Тоді в чому проблема плутанини, що змінність може призвести до уповільнення коду? Лінь Хаскелла насправді означає, що змінюваність має значення не стільки, скільки ви думаєте, плюс, що це високий рівень, тому компілятор може застосувати багато оптимізацій. Таким чином, зміна запису на місці рідко буде повільнішою, ніж це було б у такій мові, як C.


3

Чому Хаскелл (GHC) так проклятий?

Щось, мабуть, кардинально змінилося після того, як я востаннє вимірював показники роботи Haskell. Наприклад:

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

Улюблена справа Haskellers - це спробувати потрапити в межах 5% від C

Чи є у вас посилання на результати, які можна перевірити, коли хто-небудь коли-небудь потрапляв поблизу цього?


6
Хтось тричі повторив ім'я Гарропа перед дзеркалом?
Чак Адамс

2
не в 10 разів, але все-таки весь цей запис є маркетинговим шумом і виправданням. GHC справді цілком здатний наблизитися до C або навіть подолати його іноді, за швидкістю, але для цього зазвичай потрібен досить задіяний, низькорівневий стиль програмування, не сильно відрізняється від програмування в самому С. на жаль чим вище рівень коду, тим повільніше він зазвичай. витоки простору, зручні, але недостатньо ефективні типи ADT ( алгебраїчні , а не абстрактні , як це було обіцяно) тощо, тощо.
Буде Несс

1
Я щойно публікую це, бо бачив це сьогодні chrispenner.ca/posts/wc . Це реалізація утиліти wc, написаної в Haskell, яка нібито перемагає версію c.
Гарнізон

3
@Garrison дякую за посилання . 80 рядків - це те, що я назвав "стилем програмування низького рівня, не сильно відрізняється, ніж програмування в самому С". . "код вищого рівня", це був би "дурний" fmap (length &&& length . words &&& length . lines) readFile. Якщо що було швидше , ніж (або навіть порівняно з) C, галас тут буде повністю виправдана тоді . Нам все ще потрібно наполегливо працювати над швидкістю в Haskell, як і в C, справа в цьому.
Буде Несс

2
Судячи з цього обговорення на Reddit reddit.com/r/programming/comments/dj4if3/… , код Haskell дійсно є помилковим (наприклад, перерви в рядках починаються або закінчуються пробілом, перерви на à) та інші не можуть відтворити заявлені результати роботи.
Джон Харроп
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.