Ответ 1
Ответы до сих пор были сосредоточены на проблемах с производительностью seq
против pseq
, но я думаю, вы изначально хотели узнать, какой из двух вы должны использовать.
Короткий ответ: хотя оба должны генерировать почти идентично исполняемый код на практике (по крайней мере, когда правильные флаги оптимизации включены), примитивный seq
, а не pseq
, является правильным выбором для вашей ситуации. Использование pseq
не является идиоматическим, запутанным и потенциально контрпродуктивным с точки зрения производительности, и ваша причина его использования основана на ошибочном понимании того, что означает его гарантия порядка оценки и что она подразумевает в отношении производительности. Хотя нет никаких гарантий относительно производительности в разных наборах флагов компилятора (что намного меньше для других компиляторов), если вы когда-нибудь столкнетесь с ситуацией, когда версия seq
приведенного выше кода работает значительно медленнее, чем версия pseq
, используя "производство" качество "с компилятором GHC, вы должны рассмотреть его как ошибку GHC и подать отчет об ошибке.
Длительный ответ, конечно, длиннее...
Во-первых, давайте ясно, что seq
и pseq
семантически идентичны в том смысле, что оба они удовлетворяют уравнениям:
seq _|_ b = _|_
seq a b = b -- if a is not _|_
pseq _|_ b = _|_
pseq a b = b -- if a is not _|_
Это действительно единственное, что обе из них гарантируют семантически, и поскольку определение языка Haskell (как указано в отчете Haskell) дает только, в лучшем случае, семантические гарантии и не имеет отношения к производительности или в реализации нет причин выбирать между тем или другим по причинам гарантированной производительности для разных компиляторов или флагов компилятора.
Кроме того, в вашей конкретной версии sum
, основанной на seq
, нетрудно увидеть, что нет ситуации, в которой seq
вызывается с первым аргументом undefined, но с определенной секундой аргумент (предполагая использование стандартного числового типа), поэтому вы даже не используете семантические свойства seq
. Вы можете переопределить seq
как seq a b = b
и иметь точно такую же семантику. Конечно, вы знаете это - почему ваша первая версия не использовала seq
. Вместо этого вы используете seq
для побочного эффекта побочной производительности, поэтому мы вышли из сферы семантических гарантий и вернулись в область специфических реализаций компилятора GHC и характеристик производительности (там, где на самом деле нет никаких гарантий говорить).
Во-вторых, это приводит нас к цель seq
. Он редко используется для его семантических свойств, потому что эти свойства не очень полезны. Кому нужно, чтобы вычисление seq a b
возвращало b
, за исключением того, что оно не должно заканчиваться, если какое-то несвязанное выражение a
не удается завершить? (Исключения - никакие каламбуры не предназначались - будут такие вещи, как обработка исключений, где вы можете использовать seq
или deepSeq
, который основан на seq
, чтобы принудительно оценивать невыполнение выражения в неконтролируемой или контролируемой перед началом оценки другого выражения.)
Вместо этого seq a b
предназначен для принудительной оценки a
до нормальной нормальной формы головы перед возвратом результата b
, чтобы предотвратить накопление thunks. Идея заключается в том, что если у вас есть выражение b
, которое строит thunk, который может потенциально накапливаться поверх другого неоцененного thunk, представленного a
, вы можете предотвратить это накопление с помощью seq a b
. "Гарантия" является слабой: GHC гарантирует, что понимает, что вы не хотите, чтобы a
оставался неоцененным куском, когда требуется значение seq a b
. Технически это не гарантирует, что a
будет "оцениваться до" b
, что бы это ни значило, но вам не нужна эта гарантия. Когда вы беспокоитесь, что без этой гарантии GHC может сначала оценить рекурсивный вызов и все еще накапливать thunks, это так же смешно, как беспокоиться о том, что pseq a b
может оценить свой первый аргумент, а затем ждать 15 минут (просто чтобы убедиться, что первый аргумент была оценена!), прежде чем оценивать ее вторую.
Это ситуация, когда вы должны доверять GHC, чтобы поступать правильно. Может показаться вам, что единственный способ реализовать преимущества производительности seq a b
- это для a
для оценки WHNF до начала оценки b
, но вполне возможно, что в этой или других ситуациях существуют оптимизации технически начните оценивать b
(или даже полностью оценивать b
в WHNF), оставляя a
необоснованным в течение короткого времени для повышения производительности, сохраняя при этом сохранение семантики seq a b
. Используя pseq
вместо этого, вы можете запретить GHC делать такие оптимизации. (В вашей программной ситуации sum
, несомненно, нет такой оптимизации, но при более сложном использовании seq
может быть.)
В-третьих, важно понять, для чего действительно pseq
. Он впервые был описан в Marlow 2009 в контексте параллельного программирования. Предположим, мы хотим распараллелить два дорогостоящих вычисления foo
и bar
, а затем объединить (скажем, добавить) их результаты:
foo `par` (bar `seq` foo+bar) -- parens redundant but included for clarity
Цель состоит в том, что - когда это выражение требуется - оно создает искру для вычисления foo
параллельно, а затем через выражение seq
начинает оценивать bar
в WHNF (т.е. оно числовое значение, скажем), прежде чем, наконец, оценив foo+bar
, который будет ожидать от искры для foo
перед добавлением и возвратом результатов.
Здесь можно предположить, что GHC распознает, что для определенного числового типа (1) foo+bar
автоматически не завершается, если bar
делает, удовлетворяя формальной семантической гарантии seq
; и (2) оценка foo+bar
на WHNF автоматически заставит оценку bar
WHNF предотвратить любое накопление thunk и, таким образом, удовлетворить гарантию неформальной реализации seq
. В этой ситуации GHC может свободно оптимизировать seq
, чтобы получить:
foo `par` foo+bar
особенно если он считает, что было бы более результативно начинать оценку foo+bar
до завершения оценки bar
в WHNF.
Что GHC недостаточно умен, чтобы понять, что - если оценка foo
в foo+bar
начинается до начала искры foo
, искра будет гореть, и никакое параллельное выполнение не произойдет.
Это действительно только в этом случае, когда вам нужно явно задерживать требование значения искрового выражения, чтобы дать возможность ему планироваться до того, как основной поток "догонит", что вам нужна дополнительная гарантия pseq
и готовы предоставить GHC дополнительные возможности оптимизации, разрешенные более слабой гарантией seq
:
foo `par` (bar `pseq` foo+bar)
Здесь pseq
не позволит GHC вводить какую-либо оптимизацию, которая могла бы позволить foo+bar
начать оценивать (возможно, искривлять искру foo
) до того, как bar
находится в WHNF (что, мы надеемся, позволяет достаточно времени для искра должна быть запланирована).
В результате получается, что если вы используете pseq
для чего-то другого, кроме параллельного программирования, вы используете его неправильно. (Ну, может быть, есть какие-то странные ситуации, но...) Если все, что вы хотите сделать, это принудительная оценка и/или оценка thunk для повышения производительности в неконкурентном коде, используя seq
(или $!
, который определяемый в терминах seq
или строгих типов данных Haskell, которые определены в терминах $!
), является правильным подходом.
(Или, если верить оценкам @Kindaro, может быть, беспощадный бенчмаркинг с конкретными версиями и флагами компилятора - правильный подход.)