Почему новый тип Tuple в.Net 4.0 является ссылочным типом (классом), а не типом значения (структурой)
Кто-нибудь знает ответ и / или есть мнение по этому поводу?
Поскольку кортежи обычно бывают не очень большими, я предполагаю, что для них будет более разумно использовать структуры, чем классы. Что скажете вы?
5 ответов
Microsoft сделала все типы кортежей ссылочными типами в интересах простоты.
Я лично думаю, что это было ошибкой. Кортежи с более чем 4 полями очень необычны и должны быть заменены более типичной альтернативой (например, тип записи в F#), так что только маленькие кортежи представляют практический интерес. Мои собственные тесты показали, что распакованные кортежи размером до 512 байт могут быть быстрее, чем упакованные.
Хотя эффективность использования памяти является одной из проблем, я считаю, что доминирующей проблемой являются издержки сборщика мусора.NET. Выделение и сбор данных в.NET очень дороги, поскольку сборщик мусора не был сильно оптимизирован (например, по сравнению с JVM). Более того, по умолчанию.NET GC (рабочая станция) еще не распараллелена. Следовательно, параллельные программы, использующие кортежи, останавливаются, поскольку все ядра борются за общий сборщик мусора, разрушая масштабируемость. Это не только доминирующая проблема, но, AFAIK, Microsoft полностью пренебрегла, когда исследовала эту проблему.
Другая проблема - виртуальная рассылка. Ссылочные типы поддерживают подтипы и, следовательно, их члены обычно вызываются посредством виртуальной диспетчеризации. Напротив, типы значений не могут поддерживать подтипы, поэтому вызов члена полностью однозначен и всегда может быть выполнен как прямой вызов функции. Виртуальная диспетчеризация очень дорогостоящая на современном оборудовании, потому что ЦП не может предсказать, где остановится счетчик программ. JVM делает все возможное, чтобы оптимизировать виртуальную диспетчеризацию, а.NET - нет. Однако.NET обеспечивает выход из виртуальной отправки в виде типов значений. Таким образом, представление кортежей в качестве типов значений может снова значительно повысить производительность. Например, позвонив GetHashCode
для двухкратного набора в миллион раз требуется 0,17 с, но вызов его для эквивалентной структуры занимает всего 0,008 с, то есть тип значения в 20 раз быстрее, чем ссылочный тип.
Реальная ситуация, когда эти проблемы с производительностью обычно возникают при использовании кортежей, заключается в использовании кортежей в качестве ключей в словарях. Я наткнулся на эту тему, перейдя по ссылке из вопроса о переполнении стека. F# запускает мой алгоритм медленнее, чем Python! где авторская F# программа оказалась медленнее, чем его Python именно потому, что он использовал кортежи в штучной упаковке. Распаковка вручную с использованием рукописных struct
type делает его программу на F# в несколько раз быстрее и быстрее, чем Python. Эти проблемы никогда бы не возникли, если бы кортежи были представлены типами значений, а не ссылочными типами для начала...
Причина, скорее всего, заключается в том, что только меньшие кортежи будут иметь смысл в качестве типов значений, поскольку они будут иметь небольшой объем памяти. Большие кортежи (т. Е. Те, у которых больше свойств) на самом деле пострадают в производительности, так как они будут больше 16 байтов.
Вместо того, чтобы некоторые кортежи были типами значений, а другие - ссылочными типами, вынуждали разработчиков знать, что, как я думаю, люди из Microsoft думали, что сделать их все ссылочные типы проще.
Ах, подозрения подтвердились! Пожалуйста, смотрите Building Tuple:
Первое важное решение заключалось в том, следует ли рассматривать кортежи как ссылочный тип или тип значения. Поскольку они неизменяемы в любое время, когда вы хотите изменить значения кортежа, вы должны создать новый. Если они являются ссылочными типами, это означает, что может быть много мусора, если вы изменяете элементы в кортеже в узком цикле. Кортежи F# были ссылочными типами, но у команды сложилось впечатление, что они могли бы добиться улучшения производительности, если бы два, а может и три, кортежа элементов были типами значений. Некоторые команды, которые создали внутренние кортежи, использовали значения вместо ссылочных типов, потому что их сценарии были очень чувствительны к созданию большого количества управляемых объектов. Они обнаружили, что использование типа значения дало им лучшую производительность. В нашем первом проекте спецификации кортежа мы сохранили двух-, трех- и четырехэлементные кортежи как типы значений, а остальные были ссылочными типами. Тем не менее, во время совещания по проектированию, в котором участвовали представители других языков, было решено, что этот "разделенный" дизайн будет сбивать с толку из-за немного отличающейся семантики между двумя типами. Было установлено, что последовательность в поведении и дизайне имеет более высокий приоритет, чем потенциальное повышение производительности. Основываясь на этом вводе, мы изменили дизайн так, чтобы все кортежи были ссылочными типами, хотя мы попросили команду F# провести некоторое исследование производительности, чтобы увидеть, не произошло ли ускорение при использовании типа значения для некоторых размеров кортежей. У него был хороший способ проверить это, поскольку его компилятор, написанный на F#, был хорошим примером большой программы, которая использовала кортежи в различных сценариях. В конце концов, команда F# обнаружила, что она не добилась улучшения производительности, когда некоторые кортежи были типами значений вместо ссылочных типов. Это заставило нас почувствовать себя лучше относительно нашего решения использовать ссылочные типы для кортежей.
Если типы.NET System.Tuple<...>; были определены как структуры, они не будут масштабируемыми. Например, троичный кортеж из длинных целых чисел в настоящее время масштабируется следующим образом:
type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4
Если троичный кортеж был определен как структура, результат будет следующим (на основе тестового примера, который я реализовал):
sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104
Поскольку у кортежей есть встроенная поддержка синтаксиса в F#, и они очень часто используются на этом языке, кортежи "struct" поставили бы программистов F# на риск написания неэффективных программ, даже не подозревая об этом. Это случилось бы так легко:
let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3
На мой взгляд, "структурные" кортежи могут привести к значительной неэффективности в повседневном программировании. С другой стороны, существующие в настоящее время "классовые" кортежи также приводят к некоторой неэффективности, как упомянуто @Jon. Тем не менее, я думаю, что произведение "вероятности возникновения" на "потенциальный ущерб" будет намного выше для структур, чем для классов. Поэтому текущая реализация - меньшее зло.
В идеале, должны быть как кортежи "class", так и кортежи "struct", оба с синтаксической поддержкой в F#!
Изменить (2017-10-07)
Структурные кортежи теперь полностью поддерживаются следующим образом:
- Встроенный в mscorlib (.NET >= 4.7) как System.ValueTuple
- Доступен как NuGet для других версий
- Синтаксическая поддержка в C# >= 7
- Синтаксическая поддержка в F# >= 4.1
Для двух кортежей вы все равно всегда можете использовать KeyValuePair
Небольшое пояснение к статье Мэтта Эллиса заключается в том, что различие в семантике использования между ссылочным типом и типом значения является лишь "небольшим", когда действует неизменяемость (что, конечно, будет иметь место в данном случае). Тем не менее, я думаю, что в проекте BCL было бы лучше не вводить в заблуждение необходимость перехода Tuple к эталонному типу при некотором пороге.
Я не знаю, но если вы когда-либо использовали F# Кортежи являются частью языка. Если я сделал.dll и возвратил тип кортежей, было бы неплохо иметь тип для его вставки. Теперь я подозреваю, что F# является частью языка (.Net 4), были внесены некоторые изменения в CLR, чтобы приспособить некоторые общие структуры. в F#
От http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records
let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;
val scalarMultiply : float -> float * float * float -> float * float * float
scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)