откуда у параллелизма ноги
Dec. 5th, 2012 11:51 pmВ императивных языках учитывается модель памяти. Это такая вещь, которая приводит в соответствие program order и platform memory order.
Так вот. Это не функциональные программы хорошо параллелизуются, ибо иммутабельные данные. Это функциональные программы трудно писать с мутабельными данными, ибо в них остутствует этот самый program order.
Так вот. Это не функциональные программы хорошо параллелизуются, ибо иммутабельные данные. Это функциональные программы трудно писать с мутабельными данными, ибо в них остутствует этот самый program order.
no subject
Date: 2012-12-06 12:24 am (UTC)no subject
Date: 2012-12-06 02:51 am (UTC)no subject
Date: 2012-12-06 03:17 am (UTC)no subject
Date: 2012-12-06 08:12 am (UTC)Ну ещё есть вариант, что я термином злоупотребляю :)
После нескольких лет ковыряния в concurrency, смотрел на хаскелл и недоумевал - а какие вычисления будут гарантированно видимы, если, там, какой-то архитектурный барьер (ну, tvar или чё там есть - я ещё не дочитал). Потому что в Java вот есть program order, и трюк заключается в умышленном ordering каких-то вычислений по одну сторону барьера, а других - по другую сторону. В хаскелле нет такого же program order (programmer intended order; ну, или пора тогда вам все-таки разъяснительную работу провести :)).
Конечно, ленивые вычисления внутри реализованы на мутабельности, но её монотонность помогает контроллировать гонки (thunk -> value). Расстановка барьеров и порядок скедюлинг становится полностью задачей компилятора и (green thread) скедюлера. Поскольку нет программно заданных барьеров, это и хорошо (можно бить, где хочешь), и плохо (приходится заниматься микро-синхронизацией). Вот это вот "плохо" - очень сложная задача; статья 2005 года как бы намекает, что они не берутся за weaker memory platforms, ибо только на TSO существует дешёвый способ делать тот вид микро-синхронизации (racy publishing). Но я всеми руками за "хорошо": нет program order - невозможно допустить concurrency ошибку.
no subject
Date: 2012-12-06 10:38 am (UTC)Это популярное заблуждение, что в хаскеле "есть порядок, заданный зависимостями данных". В хаскеле совершенно нормальный program order, заданный точно так же статически синтаксической структурой программы, как и в любых других языках.
Источник заблуждения я не так давно обнаружил - это пейпер аж 1976 года, "A lazy evaluator" by Henderson and Morris, POPL'76. Во всяком случае, более ранние упоминания мне неизвестны.
Вот моя попытка как-то объяснить, что происходит http://stackoverflow.com/a/8012765/805266 , но на самом деле конечно you need to learn the precise semantics - just learning a metaphor doesn't let you understand the details.
no subject
Date: 2012-12-06 11:25 am (UTC)Щас проверим. Пример с (map (+1) [1..10]) !! 6 - он такой точно как и [1..10] !! 6. Случай с map отличается не mapом, а списком. Во втором случае список Integer, а в первом случае - список результатов (_ + 1). В первом случае "вычисление" закончилось тем, что сконструировало thunk для вычисления (_ + 1) для первых 5 элементов и вычислило значение из thunk одного элемента, который мы изымаем. Но! при этом значения списка [1..10] вычислены точно столько же, как и во втором случае.
Что я имею в виду иллюстрируется продолжением вашего эксперимента на stackoverflow:
Prelude> let l = [1..10] Prelude> :print l l = (_t1::[Integer]) Prelude> let m = map (+1) l Prelude> :print m m = (_t2::[Integer]) Prelude> m !! 6 8 Prelude> :print m m = (_t3::Integer) : (_t4::Integer) : (_t5::Integer) : (_t6::Integer) : (_t7::Integer) : (_t8::Integer) : 8 : (_t9::[Integer]) Prelude> :print l l = 1 : 2 : 3 : 4 : 5 : 6 : 7 : (_t10::[Integer])- да, как вы и демонстрируете, список значений m вычислился только частично. Но список значений l тоже вычислился, и вычислился до той же степени, как и в случае с просто [1..10] !! 6. Вот это, второе, и есть неочевидное, не контролируемое с точки зрения пользователя функции, поведение. Его-то и не нужно контролировать, но именно этот момент и является трудным для execution order control freaks, которыми люди становятся после серьёзных concurrency проблем в императиве. :)Я вижу большую разницу между f([1..10]) в eager вычислении и f([1..10]) в ленивом. В том-то и штука, что при eager вычислении если после f поставить барьер, не зависимо от операций внутри f, вычисленным (и видимым) будет весь список; а при lazy вычислении нельзя сказать о вычисленной (а потому видимой) части списка ничего.
no subject
Date: 2012-12-06 12:18 pm (UTC)no subject
Date: 2012-12-06 12:29 pm (UTC)no subject
Date: 2012-12-06 01:15 pm (UTC)no subject
Date: 2012-12-06 12:18 pm (UTC)no subject
Date: 2012-12-06 12:22 pm (UTC)Мне важно знать степень владения не для повышения ЧСВ, а для того, чтобы понять, поймёте ли вы мои развёрнутые ответы или нет, и не тратить зря свое и ваше время.
no subject
Date: 2012-12-06 12:32 pm (UTC)no subject
Date: 2012-12-06 10:08 am (UTC)Это ж у меня всё тезис "иммутабельность функциональщины несёт жизнь в параллельные миры" из головы не уходит. Иммутабельность можно получить и в императивщине, так почему именно функциональщина что-то спасает. И вот мне кажется, что на самом деле стимулирует повышение параллелизма не иммутабельность, а ленивость. Именно лень заставляет выражать мысль таким образом, что порядок вычислений не имеет значения (ну, минус data dependency order). И иммутабельность в ленивых вычислениях просто необходима, ибо ленивость же не даёт предсказать, как бы менялись мутабельные данные.
no subject
Date: 2012-12-06 12:14 pm (UTC)Как-то так.
no subject
Date: 2012-12-06 01:33 pm (UTC)Чистота, чёрч-россеровость, ссылочная прозрачноть тоже попадалось в списке похвал функциональщине, но я пока не понял, как это сочетается с линеаризацией.
Я на академичность не претендую, но очень польщён просвещением.
no subject
Date: 2012-12-06 02:48 pm (UTC)Нельзя. Ленивость как таковая вообще не допускает reordering. Компилятор может слегка отклоняться от порядка, предписываемого ленивостью, из-за чистоты.
Пределом разрешения reordering является сильная нормализация, которой даже в хаскеле нет, но есть в агде2. В хаскеле есть всего лишь чёрч-россеровость, более слабая форма reordering. Императивные языки тоже допускают reordering, но в ещё более слабой форме.
Расширенные возможности по reordering - это свойство чистых программ, а не ленивых. Скажем, mapreduce допускает реордеринг.
Следствием из Чёрч-россеровости является возможность отклоняться от порядка редукций, предписываемого ленивостью, если при этом не происходит зацикливания. При сильной нормализации зацикливания произойти не может при любом порядке.
Исполнять редукции выгоднее всего в аппликативном ("неленивом"), а не в нормальном ("ленивом") порядке, поэтому компилятор хаскеля пробует заменять порядок редукции на аппликативный, где это увеличит скорость, не раздует потребление памяти и не приведёт к зацикливаниям. Все эти анализы неразрешимы (как следствие - неполны) и являются оптимизациями (не могут ухудшить потребление ресурсов по сравнению с нормальным порядком), так что для 99% практических нужд можно считать, что программы на хаскеле исполняются в фиксированном нормальном порядке.
Иммутабельность способствует чистоте (и реордерингу), но возможна чистота вместе с мутабельностью (IORef в хаскеле).
К паралеллизму всё вышесказанное имеет, однако, весьма косвенное отношение.
Для паралеллизма важно, чтобы все программы из threads interleaving space имели одну и ту же семантику. Этому как раз способствует immutability. Если вы будете в хаскеле использовать IORefs (чистота без immutability) многопоточно, то получите по голове теми же проблемами с изменением семантики в зависимости от интерливинга, что и в императивных программах.
Отправными точками в дизайне хаскеля являются нестрогость и тьюринг-полнота. Затем выбрали ленивый порядок как способ реализовать нестрогость. Затем решили, что человеку трудно думать в ленивом порядке, и решили, что нужно как-то писать программы, чтобы не было необходимости всегда думать в ленивом порядке. Затем поняли, что если разрешить думать в произвольном порядке - то пропадет тьюринг-полнота, поэтому сошлись на чёрч-россеровости как компромиссном варианте. Получилось иммутабельно, и пришлось думать, как добавить мутабельность, сохранив чёрч-россеровость и предыдущие фичи. Придумали монады. Затем поняли, что получился язык, на котором очень хорошо контролировать мутабельность, и эта возможность контроля мутабельности оказалась полезна в многопоточном программировании.
no subject
Date: 2012-12-06 03:42 pm (UTC)Допускает. Это вот normal order evaluation не допускает ("всегда внутренний левый"), а lazy evaluation (graph reduction) допускает.
no subject
Date: 2012-12-06 04:15 pm (UTC)Общепринято считать семантикой функциональной части хаскеля call by need lambda calculus with whnf (формально это другое исчисление по сравнению с call by need lambda calculus with nf), а семантикой императивной - семантику из tackling the squad.
Lazy evaluation и call by need lambda calculus - это одно и то же. "Normal order reduction" - это call by name lambda calculus.
Системы редукции графов могут быть использована как формализм (clean), так и как средство реализации call by need (haskell). Общепринято как раз считать редукцию графов деталью реализации ghc. Call by need прекрасно описывается через дырки (evaluation contexts) безо всяких графов или через interaction nets, которые, насколько мне известно, не являются TGRS в общепринятом смысле.
Т.е. это вопрос договорённости. Общепринято, что семантика хаскеля (включая потребление ресурсов) - это редукция в нормальном порядке, реализация всего лишь может её оптимизировать.
Но в принципе, можно считать и что семантика хаскеля - это редукция до whnf, а какую нормализирующую стратегию использовать - неспецифицированно.
Можно спросить в кафе, пусть нас рассудят :)
no subject
Date: 2012-12-06 04:04 pm (UTC)о. вот это и есть linearizability.
"Компилятор может слегка отклоняться от порядка, предписываемого ленивостью"
Хм, я думал, ленивость не предписывает порядок, ибо статьи о реализации рассуждают о возможности исполнять что-то спекулятивно, и что есть общая очередь спарков, которые должны сейчас выполняться, и что "агенты"=железные потоки выполняют любой спарк по одному, а потому о предписываемом порядке вычислений говорить сложно.
В частности, в разделе проблем обсуждаются вопросы "как не засорить память промежуточными данными, которые потребитель не успевает проглотить". Я это воспринял как отсутствие ограничения на порядок исполнения, и лишь как оптимизацию.
Или в каком смысле тогда порядок предписан?
А ещё вот такой вопрос. Blackholing - это, конечно, всего-лишь реализация концепции не-повторения вычислений. Я затрудняюсь решить, как это называется формально, но мне кажется, это вводит в код сериализацию. В императиве как раз один из трюков - до какой-то степени разрешить дублирование вычислений. Как-то можно этим управлять в, скажем, хаскелле?
"Пределом разрешения reordering является сильная нормализация, которой даже в хаскеле нет, но есть в агде2. В хаскеле есть всего лишь чёрч-россеровость, более слабая форма reordering. Императивные языки тоже допускают reordering, но в ещё более слабой форме."
Ага, хорошо. Агду пока страшно :)
Тогда этот пост о том, что я обнаружил, что reordering в хаскеле > reordering в императиве.
Ну, и спасибо за экскурс. Теперь понятно, почему никто не мог ответить "какая в хаскелле модель памяти?" - потому что на самом деле нужно искать литературу по чёрч-россеровости и сильным норальным формам.
no subject
Date: 2012-12-06 04:41 pm (UTC)> и сильным норальным формам.
Ищите литературу по strictness analysis - это часть оптимизатора, которая решает, что можно переставлять, а что нельзя. Возможно, там будет менее академическим языком описано то, что вы ищете.
no subject
Date: 2012-12-06 05:05 pm (UTC)> реализация концепции не-повторения вычислений.
Тут важно, что ленивостью предписано, в каких случаях вычисления повторяются, а в каких нет. Это не "автоматическая дедупликация всего", а жесткие и очень примитивные синтаксические правила, вроде того как в Си жестко задано, что longFunction в int x = longFunction(); for (;;) { }; вычисляется один раз если снаружи цикла, и столько раз сколько отрабатывает цикл, если внутри.
> В императиве как раз один из трюков - до какой-то
> степени разрешить дублирование вычислений.
> Как-то можно этим управлять в, скажем, хаскелле?
Да, для тонкого управления есть strictness annotations, и в высокопроизводительном коде приходится подсказывать компилятору.
no subject
Date: 2012-12-06 05:15 pm (UTC)> хаскеле > reordering в императиве.
Ага. Но не по той причине, и к паралеллизму не относится.
> Теперь понятно, почему никто не мог ответить
> "какая в хаскелле модель памяти?"
Система типов позволяет реализовывать произвольные модели памяти, и есть достаточно много разных библиотек с разными моделями:
http://www.haskell.org/haskellwiki/Applications_and_libraries/Concurrency_and_parallelism
Есть все традиционные и все экзотические модели :)
Наиболее интересные на мой взгляд - это
Cloud Haskell
Software Transactional Memory
CHP: Communicating Haskell Processes
Repa
no subject
Date: 2012-12-06 05:27 pm (UTC)Это называется serializability (of interleaving).
> Или в каком смысле тогда порядок предписан?
В таком же смысле, в каком он предписан в императивных языках. Если вы пишете
x = 5;
y = 6;
return x;
То предписано, что сначала делается x = 5, потом y = 6, потом return x.
Но если компилятор может доказать, что перестановка ничего не меняет - то он может всё попересталять:
return 5;
y = 6;
x = 5;
Но от этого никто не говорит, что в императивных программах порядок выполнения не определён :) И при написании программы рассуждает так, как будто порядок именно такой, как он написал и не рассматривает всевозможные эквивалентные порядки.
Отличия лишь в том, что в хаскеле у оптимизатора больше свободы и программист, привыкнув к чёрч-россеровости, освобожден от необходимости думать о порядке. Освобождение от размышлений о порядке - это один из столпов освоения функционального программирования. Для начала нужно заменить временные отношения на структурные - вместо "раньше" и "позже" применять "синтаксически снаружи" и "синтаксически внутри". В-общем, для эффективного использования нужно учиться думать по-особому.
При оценке потребления ресурсов конечно приходится думать о порядке, но в 99% случаев считаешь, что порядок нормальный, и только в 1% надо пускать профайлер или смотреть в генерируемый код, чтобы узнать реальный порядок.
no subject
Date: 2012-12-06 06:05 pm (UTC)здесь пересечение терминологии, по-видимому. Я о линеаризации в смысле как здесь: Linearizability: A Correctness Condition for Concurrent Objects, M. Herlihy and J.Wing, Proceedings of 14th ACM Symposium on Principles of Programming Languages, 1987
"Но от этого никто не говорит, что в императивных программах порядок выполнения не определён :) И при написании программы рассуждает так, как будто порядок именно такой, как он написал и не рассматривает всевозможные эквивалентные порядки."
Да, понятный пример. Тогда да, я думаю лишь о реализации, дающей результат, эквивалентный какому-то там каноническому, который я ещё не изучал.
no subject
Date: 2012-12-10 12:56 pm (UTC)no subject
Date: 2012-12-10 01:29 pm (UTC)Никаким. Я же так и писал - "весьма косвенное отношение".
"Отклоняться от порядка редукций" ничего не значит, т.к. это нестрогая формулировка, объяснение на пальцах. Ознакомьтесь со строгой версией.
http://wiki.clean.cs.ru.nl/Functional_Programming_and_Parallel_Graph_Rewriting - 3 глава про системы перезаписи графов, и 5 глава про concurrency. У пиратов есть в пдф одним куском.
Но это очень маленький кусочек необходимых знаний, если хотите глубоко копать. Например, вам должна стать непонятной связь FGRS и лямбда-исчисления. Вас должно запутать, что G-machine - это не реализация FGRS.
См. также
- The Call-by-Need Lambda Calculus by John Maraist, Martin Odersky, Philip Wadler;
- http://research.microsoft.com/en-us/um/people/simonpj/papers/marktoberdorf/ (4 глава)
- http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.27.3918
и т. д.
Ещё учтите, что Parallelism != Concurrency и tackling the squad посвящён concurrency, а вы говорите именно о паралеллизме. И MPI is all about parallelism, but Erlang is all about concurrency.
Отвечая непосредственно на ваш вопрос - есть теоремы о возможности параллельных редукций с сохранением семантики.
no subject
Date: 2012-12-10 01:59 pm (UTC)no subject
Date: 2012-12-10 02:13 pm (UTC)no subject
Date: 2012-12-10 02:13 pm (UTC)Так вот, я придумал как перевести исходный текст поста на нормальный язык. Будет так:Хаскел - это чистый функциональный язык с контролируемыми эффектами, поощрающий писать программы, доказуемо эффектов не имеющие.
В принципе, можно придумать чистый логический язык, императивный язык с контролируемыми эффектами, язык с программированием в ограничениях, чистый реляционный язык и т.п. Т.е. функциональность как таковая не имеет к параллелизуемости отношения.
Важна
а) возможность писать программы, доказуемо не имеющие эффектов
б) экономическая целесообразность писать большие программы доказуемо без эффектов
Пункт б) как раз в Хаскеле подкреплен ленивостью, чистотой, немутабельностью, типизированностью, выводом типов и другими полезными фишками. Но именно что "подкреплён". Основное - это пункт а)