Затраты на исключительную ситуацию 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 если ключевой элемент не присутствовал в коллекции.

Другие вопросы по тегам