Влияет ли использование типов размеченных объединений с одним регистром на производительность?

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


person Trident D'Gao    schedule 15.09.2013    source источник


Ответы (3)


Да, производительность будет падать при использовании типов объединения в одном регистре для переноса примитивных значений. Юнион-кейсы компилируются в классы, так что вы будете платить за выделение (а позже и сбор) класса, а также будете иметь дополнительную косвенность каждый раз, когда будете извлекать значение, хранящееся внутри юнион-кейса.

В зависимости от специфики вашего приложения и того, как часто вы будете нести эти дополнительные накладные расходы, это все же может быть целесообразно, если это сделает ваш код более безопасным и более модульным.

Я написал много кода, чувствительного к производительности, на F#, и лично я предпочитаю использовать единицы измерения F#, когда это возможно, для "пометки" примитивных типов (например, int). Это предохраняет их от неправильного использования (благодаря средству проверки типов компилятора F#), а также позволяет избежать каких-либо дополнительных накладных расходов во время выполнения, поскольку типы мер стираются при компиляции кода. Если вам нужны примеры этого, я использовал этот шаблон проектирования широко в моем fsharp-tools проекты.

person Jack P.    schedule 15.09.2013
comment
@bonomo Вы можете, но это немного хакерски. Вы также должны иметь в виду, что, поскольку типы единиц измерения стираются, они не применяются во время выполнения; если вам это нужно, вы застряли, создавая случаи объединения или какой-либо другой тип для переноса ваших значений. См. это обсуждение для получения дополнительной информации: it">Ограничения времени компиляции для строк в F#, аналогичные единицам измерения — возможно ли это? - person Jack P.; 15.09.2013
comment
... выделение... косвенное... и барьер записи при изменении ссылочного типа в куче. - person J D; 21.09.2013
comment
вы не можете скрыть конструктор или принудительно проверить единицы измерения, верно? так что -1‹celcius› не может быть создан? - person Maslow; 18.06.2014
comment
В F# 4.1 теперь есть структурные объединения с разделением по регистру с использованием аннотации [‹Struct›], см. RFC FS-1014: github.com/fsharp/fslang-design/blob/master/FSharp-4.1/ — производительность значительно увеличена. - person chillitom; 12.04.2017

У Джека гораздо больше опыта написания высокопроизводительного кода на 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%). И это смотрит на тест, который практически ничего не делает, кроме развертывания значений в цикле. В коде, который делает что-то полезное, накладные расходы будут намного меньше.

Это может быть проблемой, если вы пишете высокопроизводительный код, что-то, что будет обрабатывать много данных, или что-то, что требует некоторого времени, и пользователи будут часто запускать его (например, компилятор и инструменты). С другой стороны, если ваше приложение не критично к производительности, то это вряд ли будет проблемой.

person Tomas Petricek    schedule 16.09.2013
comment
В C# нет такого понятия, как размеченные объединения. Вместо этого можно использовать шаблон посетителя, который включает создание посетителя и вызов виртуальной функции (может быть более одного) каждый раз, когда вам нужно преобразовать абстрактный экземпляр в конкретный. Я понятия не имею, каков внутренний диспетчерский механизм дискриминированных профсоюзов. Хотел бы я знать это, чтобы принимать более осознанные решения об их использовании. Я буду признателен, если у вас есть какие-либо идеи по этому поводу. - person Trident D'Gao; 16.09.2013
comment
Кто-то декомпилировал размеченный союз. Это действительно основано на наследовании, хотя диспетчеризация выполняется путем проверки логических свойств и регистра типов (я подозреваю), что хорошо для нескольких случаев, но может быть проблемой при большом количестве множественных случаев. bugfree.dk /блог/23/06/2012/ - person Trident D'Gao; 16.09.2013
comment
Сопоставление с образцом компилируется с использованием свойства Tag (по сути, это switch над целочисленным значением). Это прекрасно подходит для нескольких случаев — размеченные объединения позволяют легко добавлять новые функции, что невозможно при использовании виртуальных методов. Основным преимуществом посетителей является удобство сопровождения — это не проблема для кода, сгенерированного компилятором. - person Tomas Petricek; 16.09.2013
comment
Array.init 1000000 (fun n -> Foo -n) |> Array.sort в 90 раз медленнее, чем Array.init 1000000 (fun n -> -n) |> Array.sort. - person J D; 21.09.2013
comment
@JonHarrop Интересно, не потому ли это, что автоматически сгенерированное сравнение размеченных союзов неэффективно? Было бы интересно сравнить это с Array.sortBy (fun (Foo n) -> -n). Это может быть более реалистично. - person Tomas Petricek; 24.09.2013
comment
@TomasPetricek: Это, безусловно, часть этого. Ваше решение в 20 раз быстрее, хотя я не уверен, почему. Я также сравнил со структурой, и это также намного быстрее, чем объединение, несмотря на сравнение, созданное компилятором. - person J D; 24.09.2013
comment
@TomasPetricek: проблема, похоже, в том, что Array.sort не был определен как let inline sort xs = Array.sortWith compare xs, а скорее отбрасывает информацию о типе времени компиляции, а затем использует общее сравнение во время выполнения. - person J D; 24.09.2013

Начиная с F# 4.1 и далее добавление атрибута [<Struct>] к подходящим объединениям с разграничением по одному регистру повысит производительность и уменьшит количество выполняемых выделений памяти.

person chillitom    schedule 12.04.2017