Хорошо иметь оболочку для каждого примитивного значения, чтобы не было возможности использовать ее не по назначению. Подозреваю, что за это удобство приходится платить. Есть ли падение производительности? Должен ли я вместо этого использовать голые примитивные значения, если производительность вызывает беспокойство?
Влияет ли использование типов размеченных объединений с одним регистром на производительность?
Ответы (3)
Да, производительность будет падать при использовании типов объединения в одном регистре для переноса примитивных значений. Юнион-кейсы компилируются в классы, так что вы будете платить за выделение (а позже и сбор) класса, а также будете иметь дополнительную косвенность каждый раз, когда будете извлекать значение, хранящееся внутри юнион-кейса.
В зависимости от специфики вашего приложения и того, как часто вы будете нести эти дополнительные накладные расходы, это все же может быть целесообразно, если это сделает ваш код более безопасным и более модульным.
Я написал много кода, чувствительного к производительности, на F#, и лично я предпочитаю использовать единицы измерения F#, когда это возможно, для "пометки" примитивных типов (например, int). Это предохраняет их от неправильного использования (благодаря средству проверки типов компилятора F#), а также позволяет избежать каких-либо дополнительных накладных расходов во время выполнения, поскольку типы мер стираются при компиляции кода. Если вам нужны примеры этого, я использовал этот шаблон проектирования широко в моем fsharp-tools проекты.
У Джека гораздо больше опыта написания высокопроизводительного кода на F#, чем у меня, поэтому я думаю, что его ответ абсолютно правильный (я также думаю, что идея использовать единицы измерения довольно интересна).
Просто чтобы понять контекст, я написал очень простой тест (используя только F # Interactive, поэтому в режиме Release все может отличаться) для сравнения производительности. Он выделяет массив обернутых (по сравнению с не обернутыми) значений int
. Вероятно, это сценарий, в котором типы без обертки действительно являются хорошим выбором, потому что массив будет просто непрерывным блоком памяти.
#time
// Using a simple wrapped `int` type
type Foo = Foo of int
let foos = Array.init 1000000 Foo
// Add all 'foos' 1k times and ignore the results
for i in 0 .. 1000 do
let mutable total = 0
for Foo f in foos do total <- total + f
На моей машине цикл for
занимает в среднем около 1050 мс. Теперь развернутая версия:
let bars = Array.init 1000000 id
for i in 0 .. 1000 do
let mutable total = 0
for b in bars do total <- total + b
На моей машине это занимает около 700 мс.
Так что, безусловно, есть некоторое снижение производительности, но, возможно, меньшее, чем можно было бы ожидать (около 33%). И это смотрит на тест, который практически ничего не делает, кроме развертывания значений в цикле. В коде, который делает что-то полезное, накладные расходы будут намного меньше.
Это может быть проблемой, если вы пишете высокопроизводительный код, что-то, что будет обрабатывать много данных, или что-то, что требует некоторого времени, и пользователи будут часто запускать его (например, компилятор и инструменты). С другой стороны, если ваше приложение не критично к производительности, то это вряд ли будет проблемой.
Tag
(по сути, это switch
над целочисленным значением). Это прекрасно подходит для нескольких случаев — размеченные объединения позволяют легко добавлять новые функции, что невозможно при использовании виртуальных методов. Основным преимуществом посетителей является удобство сопровождения — это не проблема для кода, сгенерированного компилятором.
- person Tomas Petricek; 16.09.2013
Array.init 1000000 (fun n -> Foo -n) |> Array.sort
в 90 раз медленнее, чем Array.init 1000000 (fun n -> -n) |> Array.sort
.
- person J D; 21.09.2013
Array.sortBy (fun (Foo n) -> -n)
. Это может быть более реалистично.
- person Tomas Petricek; 24.09.2013
Array.sort
не был определен как let inline sort xs = Array.sortWith compare xs
, а скорее отбрасывает информацию о типе времени компиляции, а затем использует общее сравнение во время выполнения.
- person J D; 24.09.2013
Начиная с F# 4.1 и далее добавление атрибута [<Struct>]
к подходящим объединениям с разграничением по одному регистру повысит производительность и уменьшит количество выполняемых выделений памяти.