Исключение CLR vs OCaml

Чтение Начало F # - Роберт Пикеринг Я сосредоточился на следующем абзаце:

Программисты, выходящие из фона OCaml, должны быть осторожны, когда используя исключения в F #. Из-за архитектуры CLR, бросать исключение довольно дорого - довольно немного дороже чем в OCaml. Если вы забрасываете много исключений, прокомментируйте свой код тщательно решить, стоит ли стоимость исполнения. Если слишком высокие затраты, соответствующим образом переработайте код.

Почему из-за CLR бросание исключения дороже, если F# чем в OCaml? И как лучше всего пересмотреть код в этом случае?

Ответ 1

Рид уже объяснил, почему исключения .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 на С#):

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 часто делает код намного короче, поэтому он предпочтительнее. Это может быть немного менее эффективно, чем использование рекурсии напрямую, но в большинстве случаев вам не нужно беспокоиться об этом.

EDIT: Мне было интересно узнать о фактической производительности. Версия, использующая Seq.tryFind, более эффективна, если ввод представляет собой ленивую последовательность seq { 1 .. 100 } вместо списка [ 1 .. 100 ] (из-за затрат на распределение списков). С этими изменениями и функцией test, которая возвращает 25-й элемент, время, необходимое для запуска кода 100000 раз на моей машине, следующее:

exceptions   2.400sec  
recursion    0.013sec  
Seq.tryFind  0.240sec

Это чрезвычайно тривиальный пример, поэтому я думаю, что решение с использованием Seq обычно не будет работать в 10 раз медленнее, чем эквивалентный код, написанный с использованием рекурсии. Замедление, вероятно, связано с распределением дополнительных структур данных (объект, представляющий последовательность, замыкания,...), а также из-за дополнительной косвенности (для этого требуется множество вызовов виртуальных методов вместо простых числовых операций и переходов). Однако исключения являются еще более дорогостоящими и не делают код короче или читабельнее каким-либо образом...

Ответ 2

Исключения в CLR очень богаты и предоставляют много деталей. Рико Мариани опубликовал (старый, но все еще релевантный) пост в стоимость исключений в CLR, в котором подробно описывается это.

Таким образом, исключение исключения в CLR имеет более высокую относительную стоимость, чем в некоторых других средах, включая OCaml.

И как лучше всего пересмотреть код в этом случае?

Если вы ожидаете, что исключение будет поднято в нормальных, исключительных обстоятельствах, вы можете пересмотреть свой алгоритм и API таким образом, чтобы полностью исключить исключение. Попробуйте предоставить альтернативный API, где вы можете проверить это обстоятельство до того, как, например, возникнет исключение.

Ответ 3

Почему из-за CLR бросание исключения дороже, если F #, чем в OCaml?

OCaml был сильно оптимизирован для использования исключений в качестве потока управления. Напротив, исключения в .NET не были оптимизированы вообще.

Обратите внимание, что разница в производительности огромна. Исключения в OCaml примерно на 600 × быстрее, чем в F #. Даже С++ в этом отношении примерно на 6 × медленнее, чем OCaml, в соответствии с моими показателями.

Даже несмотря на то, что исключения .NET предположительно предоставляют больше (OCaml предоставляет трассировки стека, чего еще вы хотите?) Я не могу придумать, почему они должны быть такими медленными, как они есть.

И как лучше всего пересмотреть код в этом случае?

В F # вы должны написать "общие" функции. Это означает, что ваша функция должна возвращать значение типа объединения, указывающее вид результата, например. нормальный или исключительный.

В частности, вызовы find должны быть заменены на вызовы tryFind, которые возвращают значение типа option, либо значение Some, либо None, если ключевой элемент не присутствовал в коллекция.