Ответ 1
Я думаю, что текущие гарантии модели памяти Java делают довольно трудным сделать много, если таковые имеются, автоматическое распараллеливание в компиляторе или виртуальной машине уровень. Язык Java не имеет семантики, чтобы гарантировать, что любая структура данных даже эффективно неизменна или что любой конкретный оператор является чистым и свободным от побочных эффектов, поэтому компилятор должен был бы автоматически вычислить их автоматически, чтобы распараллелить. Некоторые элементарные возможности можно было бы сделать возможным в компиляторе, но общий случай будет оставлен во время выполнения, поскольку динамическая загрузка и привязка могут вносить новые мутации, которых не было во время компиляции.
Рассмотрим следующий код:
for (int i = 0; i < array.length; i++) {
array[i] = expensiveComputation(array[i]);
}
Было бы тривиально распараллеливаться, если expensiveComputation
является чистой функцией, выход которой зависит только от его аргумента, и если бы мы могли что array
не будет изменен во время цикла (на самом деле мы его меняем, устанавливаем array[i]=...
, но в этом конкретном случае expensiveComputation(array[i])
всегда вызывается первым, так что здесь все в порядке - если предположить, что array
является локальным и не ссылаются нигде).
Кроме того, если мы изменим такой цикл:
for (int i = 0; i < array.length; i++) {
array[i] = expensiveComputation(array, i);
// expensiveComputation has the whole array at its disposal!
// It could read or write values anywhere in it!
}
то распараллеливание уже не тривиально, даже если expensiveComputation
является чистым и не изменяет его аргумент, потому что параллельные потоки будут изменять содержимое array
, а другие читают его! Параллелизатор должен был бы выяснить, какие части массива expensiveComputation
относятся к различным условиям, и синхронизировать соответственно.
Возможно, было бы совершенно невозможно обнаружить все мутации и побочные эффекты, которые могут произойти, и учитывать их при распараллеливании, но это было бы очень сложно, наверняка, вероятно, на практике невозможно. Вот почему распараллеливание и выяснение того, что все по-прежнему работает правильно, является головной болью программиста в Java.
Функциональные языки (например, Clojure на JVM) являются горячим ответом на эту тему. Чистые, свободные от побочных эффектов функции вместе с persistent ( "эффективно неизменяемые" ) структуры данных потенциально допускают неявное или почти неявное распараллеливание. Пусть двойной каждый элемент массива:
(map #(* 2 %) [1 2 3 4 5])
(pmap #(* 2 %) [1 2 3 4 5]) ; The same thing, done in parallel.
Это прозрачно из-за двух вещей:
- Функция
#(* 2 %)
чиста: она принимает значение in и дает значение, и что оно. Это ничего не меняет, и его вывод зависит только от его аргумента. - Вектор
[1 2 3 4 5]
неизменен: независимо от того, кто смотрит на него или когда он тот же.
Возможно создание чистых функций в Java, но 2), неизменность, является ахиллесовой пятой здесь. На Java нет неизменяемых массивов. Чтобы быть педантом, в Java нет ничего невозможного, потому что даже теги final
могут быть изменены с помощью отражения. Поэтому не может быть никаких гарантий, что вывод (или вход!) Вычисления не будет изменен путем распараллеливания → поэтому автоматическая распараллеливание, как правило, неосуществимо.
Немой пример "удвоения элементов" распространяется на произвольно сложную обработку благодаря неизменности:
(defn expensivefunction [v x]
(/ (reduce * v) x))
(let [v [1 2 3 4 5]]
(map (partial expensivefunction v) v)) ; pmap would work equally well here!