Затраты на исключительную ситуацию CLR и OCaml
Чтение Начало F# - Роберт Пикеринг Я сосредоточился на следующем абзаце:
Программисты из OCaml должны быть осторожны при использовании исключений в F#. Из-за архитектуры CLR создание исключения довольно дорого - немного дороже, чем в OCaml. Если вы добавляете много исключений, тщательно профилируйте свой код, чтобы решить, стоит ли затраты производительности. Если затраты слишком высоки, пересмотрите код соответствующим образом.
Почему, из-за CLR, генерировать исключение дороже, если F#
чем в OCaml
? И каков наилучший способ надлежащим образом пересмотреть код в этом случае?
3 ответа
Рид уже объяснил, почему исключения.NET ведут себя иначе, чем исключения OCaml. В целом исключения.NET подходят только для исключительных ситуаций и предназначены для этой цели. OCaml имеет более легкую модель, поэтому они также используются для реализации некоторых шаблонов потока управления.
Чтобы привести конкретный пример, в OCaml вы можете использовать исключение для реализации прерывания цикла. Например, скажем, у вас есть функция test
который проверяет, является ли число числом, которое мы хотим. Следующее перебирает числа от 1 до 100 и возвращает первый соответствующий номер:
// Simple exception used to return the result
exception Returned of int
try
// Iterate over numbers and throw if we find matching number
for n in 0 .. 100 do
printfn "Testing: %d" n
if test n then raise (Returned n)
-1 // Return -1 if not found
with Returned r -> r // Return the result here
Чтобы реализовать это без исключений, у вас есть два варианта. Вы можете написать рекурсивную функцию с таким же поведением - если вы вызываете find 0
(и он скомпилирован по существу с тем же кодом IL, что и при использовании return n
внутри for
цикл в C#):
let rec find n =
printfn "Testing: %d" n
if n > 100 then -1 // Return -1 if not found
elif test n then n // Return the first found result
else find (n + 1) // Continue iterating
Кодирование с использованием рекурсивных функций может быть немного длинным, но вы также можете использовать стандартные функции, предоставляемые библиотекой F#. Часто это лучший способ переписать код, который будет использовать исключения OCaml для потока управления. В этом случае вы можете написать:
// Find the first value matching the 'test' predicate
let res = seq { 0 .. 100 } |> Seq.tryFind test
// This returns an option type which is 'None' if the value
// was not found and 'Some(x)' if the value was found.
// You can use pattern matching to return '-1' in the default case:
match res with
| None -> -1
| Some n -> n
Если вы не знакомы с типами опций, взгляните на некоторые вводные материалы. В F# wikibook есть хороший учебник, а в документации MSDN есть и полезные примеры.
Используя соответствующую функцию из Seq
Модуль часто делает код намного короче, поэтому он предпочтительнее. Это может быть немного менее эффективно, чем прямое использование рекурсии, но в большинстве случаев вам не нужно беспокоиться об этом.
РЕДАКТИРОВАТЬ: мне было любопытно о фактической производительности. Версия, использующая Seq.tryFind
более эффективно, если на входе лениво генерируется последовательность seq { 1 .. 100 }
вместо списка [ 1 .. 100 ]
(из-за затрат на размещение списка). С этими изменениями и test
Функция, которая возвращает 25-й элемент, время, необходимое для выполнения кода 100000 раз на моей машине:
exceptions 2.400sec
recursion 0.013sec
Seq.tryFind 0.240sec
Это очень тривиальный пример, поэтому я думаю, что решение с использованием Seq
обычно не работает в 10 раз медленнее, чем эквивалентный код, написанный с использованием рекурсии. Замедление, вероятно, связано с выделением дополнительных структур данных (объект, представляющий последовательность, замыкания,...), а также из-за дополнительного косвенного обращения (код требует многочисленных вызовов виртуальных методов вместо простых числовых операций и переходов). Тем не менее, исключения являются еще более дорогостоящими и не делают код короче или более читабельным в любом случае...
Исключения в CLR очень богаты и содержат много деталей. Rico Mariani опубликовал (старый, но все еще актуальный) пост о стоимости исключений в CLR, в котором подробно излагаются некоторые из них.
Таким образом, создание исключения в CLR имеет более высокую относительную стоимость, чем в некоторых других средах, включая OCaml.
И каков наилучший способ надлежащим образом пересмотреть код в этом случае?
Если вы ожидаете, что исключение будет возбуждено в обычных, не исключительных обстоятельствах, вы можете переосмыслить свой алгоритм и API таким образом, чтобы полностью исключить исключение. Попробуйте предоставить альтернативный API, в котором вы можете проверить, например, обстоятельства, прежде чем вызывать исключение.
Почему из-за CLR генерирование исключения обходится дороже, если F#, чем в OCaml?
OCaml был сильно оптимизирован для использования исключений в качестве потока управления. Напротив, исключения в.NET вообще не были оптимизированы.
Обратите внимание, что разница в производительности огромна. Исключения примерно в 600 раз быстрее в OCaml, чем в F#. По моим оценкам, даже C++ примерно в 6 раз медленнее, чем OCaml.
Несмотря на то, что исключения.NET якобы предоставляют больше (OCaml предоставляет трассировки стека, что еще вы хотите?) Я не могу придумать ни одной причины, почему они должны быть такими же медленными, как и они.
И каков наилучший способ надлежащим образом пересмотреть код в этом случае?
В F# вы должны написать "итоговые" функции. Это означает, что ваша функция должна возвращать значение типа объединения, указывающее тип результата, например, обычный или исключительный.
В частности, звонки на find
следует заменить на звонки tryFind
которые возвращают значение option
типа, который либо Some
значение или None
если ключевой элемент не присутствовал в коллекции.