Почему новый тип Tuple в .Net 4.0 является ссылочным типом (классом), а не типом значения (структурой)

89

Кто-нибудь знает ответ и / или имеет мнение по этому поводу?

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

Бент Расмуссен
источник
1
Для всех, кто застрял здесь после 2016 года. В C # 7 и новее литералы Tuple относятся к семейству типов ValueTuple<...>. См. Ссылку на типы кортежей C #
Тамир Даниэли

Ответы:

94

Microsoft сделала все типы кортежей ссылочными типами в интересах простоты.

Я лично считаю это ошибкой. Кортежи с более чем 4 полями очень необычны и в любом случае должны быть заменены более типизированной альтернативой (например, тип записи в F #), поэтому практический интерес представляют только небольшие кортежи. Мои собственные тесты показали, что неупакованные кортежи размером до 512 байт все еще могут быть быстрее, чем упакованные кортежи.

Хотя эффективность памяти является одной из проблем, я считаю, что основной проблемой являются накладные расходы сборщика мусора .NET. Размещение и сборка в .NET обходятся очень дорого, потому что его сборщик мусора не был оптимизирован очень сильно (например, по сравнению с JVM). Более того, стандартный .NET GC (рабочая станция) еще не распараллеливался. Следовательно, параллельные программы, использующие кортежи, перестают работать, поскольку все ядра борются за общий сборщик мусора, разрушая масштабируемость. Это не только основная проблема, но, AFAIK, Microsoft полностью проигнорировала эту проблему.

Еще одна проблема - виртуальная отправка. Ссылочные типы поддерживают подтипы, поэтому их члены обычно вызываются через виртуальную диспетчеризацию. Напротив, типы значений не могут поддерживать подтипы, поэтому вызов члена полностью однозначен и всегда может выполняться как прямой вызов функции. Виртуальная диспетчеризация обходится очень дорого на современном оборудовании, потому что ЦП не может предсказать, где окажется счетчик программ. JVM делает все возможное для оптимизации виртуальной диспетчеризации, а .NET - нет. Однако .NET обеспечивает выход из виртуальной диспетчеризации в виде типов значений. Таким образом, представление кортежей как типов значений могло бы, опять же, значительно улучшить производительность. Например, позвонивGetHashCode для кортежа из 2 миллионов раз требуется 0,17 с, но для его вызова в эквивалентной структуре требуется всего 0,008 с, т.е. тип значения в 20 раз быстрее, чем ссылочный тип.

Реальная ситуация, когда эти проблемы с производительностью кортежей обычно возникают, заключается в использовании кортежей в качестве ключей в словарях. Я фактически наткнулся на эту ветку, перейдя по ссылке из вопроса о переполнении стека. F # выполняет мой алгоритм медленнее, чем Python! где авторская программа на F # оказалась медленнее, чем его Python именно потому, что он использовал коробочные кортежи. Распаковка вручную с использованием рукописного structшрифта делает его программу F # в несколько раз быстрее и быстрее, чем Python. Этих проблем никогда бы не возникло, если бы кортежи были представлены типами значений, а не ссылочными типами для начала ...

JD
источник
2
@Bent: Да, именно это я и делаю, когда сталкиваюсь с кортежами на горячем пути в F #. Было бы неплохо, если бы они предоставили как упакованные, так и распакованные кортежи в .NET Framework ...
JD
18
Что касается виртуальной отправки, я думаю, что ваша вина неуместна: Tuple<_,...,_>типы могли быть запечатаны, и в этом случае виртуальная отправка не потребовалась бы, несмотря на то, что они являются ссылочными типами. Мне больше любопытно, почему они не запечатаны, чем почему они являются ссылочными типами.
kvb
2
Судя по моему тестированию, для сценария, в котором кортеж будет сгенерирован в одной функции и возвращен в другую функцию, а затем больше никогда не будет использоваться, структуры с открытым полем, похоже, обеспечивают превосходную производительность для элемента данных любого размера, который не настолько велик, чтобы взорвать стек. Неизменяемые классы лучше, только если ссылки будут передаваться достаточно, чтобы оправдать их стоимость построения (чем больше элемент данных, тем меньше их нужно передавать, чтобы компромисс в пользу них). Поскольку предполагается, что кортеж представляет собой просто набор связанных вместе переменных, структура может показаться идеальной.
supercat
2
«Неупакованные кортежи размером до 512 байт могут быть быстрее, чем упакованные» - что это за сценарий? Вы могли бы быть в состоянии выделить на структуру 512B быстрее экземпляра класса , проведение 512b данных, но передать его вокруг будет более чем в 100 раз медленнее (предполагается , что x86). Я что-то не замечаю?
Groo
45

Причина, скорее всего, заключается в том, что только кортежи меньшего размера будут иметь смысл в качестве типов значений, поскольку они будут иметь небольшой объем памяти. Кортежи большего размера (т.е. кортежи с большим количеством свойств) фактически пострадали бы от производительности, поскольку они были бы больше 16 байт.

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

Ах, подозрения подтвердились! Пожалуйста, смотрите Building Tuple :

Первым важным решением было рассматривать кортежи как ссылочный или как значение. Поскольку они неизменяемы каждый раз, когда вы хотите изменить значения кортежа, вам нужно создать новый. Если это ссылочные типы, это означает, что при изменении элементов кортежа в жестком цикле может образоваться много мусора. Кортежи F # были ссылочными типами, но у команды было ощущение, что они могли бы добиться улучшения производительности, если бы вместо них два, а возможно, и три кортежа элементов были типами значений. Некоторые группы, создававшие внутренние кортежи, использовали значения вместо ссылочных типов, потому что их сценарии были очень чувствительны к созданию большого количества управляемых объектов. Они обнаружили, что использование типа значения дает им лучшую производительность. В нашем первом черновике спецификации кортежа мы сохранили двух-, трех- и четырехэлементные кортежи как типы значений, а остальные были ссылочными типами. Однако во время встречи по дизайну, на которой присутствовали представители других языков, было решено, что этот «разделенный» дизайн будет сбивать с толку из-за немного различающейся семантики между двумя типами. Последовательность в поведении и дизайне была определена как более высокий приоритет, чем возможное повышение производительности. Основываясь на этих входных данных, мы изменили дизайн так, чтобы все кортежи были ссылочными типами, хотя мы попросили команду F # провести некоторое исследование производительности, чтобы увидеть, не произошло ли ускорение при использовании типа значения для некоторых размеров кортежей. У него был хороший способ проверить это, поскольку его компилятор, написанный на F #, был хорошим примером большой программы, которая использовала кортежи в самых разных сценариях. В конце концов, команда разработчиков F # обнаружила, что не происходит повышения производительности, когда некоторые кортежи были типами значений, а не ссылочными типами. Это заставило нас почувствовать себя лучше, приняв решение использовать ссылочные типы для кортежа.

Эндрю Хэйр
источник
3
Отличное обсуждение здесь: blogs.msdn.com/bclteam/archive/2009/07/07/…
Кит Адлер,
АА, вижу. Я все еще немного смущен тем, что типы значений здесь ничего не значат на практике: P
Bent Rasmussen
Я только что прочитал комментарий об отсутствии универсальных интерфейсов, и когда я посмотрел на код ранее, это было совершенно другое, что меня поразило. На самом деле довольно скучно, насколько неинтересны типы Tuple. Но, я думаю, вы всегда можете создать свой собственный ... В C # и так нет синтаксической поддержки. Но по крайней мере ... Тем не менее, использование дженериков и ограничений, которые они имеют, все еще кажется ограниченным в .Net. Существует значительный потенциал для очень общих очень абстрактных библиотек, но для универсальных библиотек, вероятно, потребуются дополнительные вещи, такие как ковариантные возвращаемые типы.
Бент Расмуссен,
7
Ваш предел в 16 байт - подделка. Когда я тестировал это на .NET 4, я обнаружил, что сборщик мусора настолько медленный, что распакованные кортежи размером до 512 байт все еще могут быть быстрее. Я бы также сомневался в результатах тестов Microsoft. Готов поспорить, они проигнорировали параллелизм (компилятор F # не является параллельным), и именно здесь отказ от GC действительно окупается, потому что GC рабочей станции .NET также не является параллельным.
JD
Из любопытства интересно, проверила ли команда компиляторов идею превращения кортежей в структуры EXPOSED-FIELD ? Если у кого-то есть экземпляр типа с различными характеристиками и ему нужен экземпляр, который идентичен, за исключением одного отличия, структура с открытым полем может выполнить это намного быстрее, чем любой другой тип, и преимущество только растет по мере того, как больше.
supercat
7

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

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

На мой взгляд, "структурные" кортежи с большой вероятностью вызовут значительную неэффективность в повседневном программировании. С другой стороны, существующие в настоящее время кортежи «классов» также вызывают определенную неэффективность, как упоминал @Jon. Однако я думаю, что произведение «вероятность возникновения» на «потенциальный ущерб» будет намного выше для структур, чем для классов. Следовательно, текущая реализация - меньшее зло.

В идеале должны быть как кортежи «классов», так и кортежи «структуры», оба с синтаксической поддержкой в ​​F #!

Изменить (2017-10-07)

Кортежи структур теперь полностью поддерживаются следующим образом:

  • Встроен в mscorlib (.NET> = 4.7) как System.ValueTuple
  • Доступен как NuGet для других версий
  • Синтаксическая поддержка в C #> = 7
  • Синтаксическая поддержка в F #> = 4.1
Марк Сигрист
источник
2
Если избежать ненужного копирования, структура открытого поля любого размера будет более эффективной, чем неизменяемый класс того же размера, если только каждый экземпляр не будет скопирован столько раз, что стоимость такого копирования превысит стоимость создания объекта кучи ( безубыточное количество копий зависит от размера объекта). Такое копирование может быть неизбежным , если один хочет - структуру , которая претендует быть неизменны, но Структуры , которая предназначена появляться в виде набора переменных (что структура есть ) можно эффективно использовать даже тогда , когда они огромны.
supercat
2
Может случиться так, что F # не очень хорошо сочетается с идеей передачи структур ref, или может не понравиться тот факт, что так называемые «неизменяемые структуры» нет, особенно когда они упакованы. Жаль, что .net никогда не реализовывал концепцию передачи параметров принудительным элементом const ref, поскольку во многих случаях такая семантика - это то, что действительно требуется.
supercat
1
Кстати, амортизированную стоимость GC я рассматриваю как часть стоимости размещения объектов; если после каждого мегабайта распределений потребуется сборщик мусора L0, то затраты на выделение 64 байтов составляют примерно 1/16 000 стоимости сборщика мусора L0 плюс часть стоимости любых сборщиков мусора L1 или L2, которые необходимы в качестве следствие этого.
supercat
4
«Я думаю, что произведение вероятности возникновения, умноженное на потенциальный ущерб, будет намного выше для структур, чем сейчас для классов». FWIW, я очень редко видел кортежи кортежей в дикой природе и считаю их недостатком дизайна, но я очень часто вижу, как люди борются с ужасной производительностью при использовании (ref) кортежей в качестве ключей в a Dictionary, например здесь: stackoverflow.com/questions/5850243 /…
JD
3
@Jon Прошло два года с тех пор, как я написал этот ответ, и теперь я согласен с вами, что было бы предпочтительнее, если бы хотя бы 2- и 3-кортежи были структурами. В этой связи было сделано голосовое предложение пользователя на языке F # . Этот вопрос имеет некоторую срочность, поскольку в последние годы наблюдается массовый рост приложений в области больших данных, количественных финансов и игр.
Marc Sigrist
4

Для 2-кортежей вы всегда можете использовать KeyValuePair <TKey, TValue> из более ранних версий Common Type System. Это ценностный тип.

Небольшое уточнение к статье Мэтта Эллиса будет заключаться в том, что разница в семантике использования между ссылочными типами и типами значений является лишь «незначительной», когда действует неизменяемость (что, конечно, будет иметь место здесь). Тем не менее, я думаю, что было бы лучше в дизайне BCL не вводить путаницу, связанную с переходом Tuple к ссылочному типу на некотором пороге.

Гленн Слейден
источник
Если значение будет использовано один раз после его возврата, структура открытого поля любого размера будет превосходить любой другой тип, только при условии, что она не настолько чудовищно огромна, чтобы взорвать стек. Стоимость создания объекта класса будет возмещена только в том случае, если ссылка будет использоваться совместно несколько раз. Бывают случаи, когда для универсального гетерогенного типа фиксированного размера полезно быть классом, но в других случаях структура была бы лучше - даже для «больших» вещей.
supercat
Спасибо за добавление этого полезного практического правила. Однако я надеюсь, что вы правильно поняли мою позицию: я наркоман с ценностями. ( stackoverflow.com/a/14277068 не должен оставлять сомнений).
Гленн Слейден,
Типы значений - одна из замечательных особенностей .net, но, к сожалению, человек, написавший msdn dox, не смог распознать, что для них существует несколько несвязанных вариантов использования и что разные варианты использования должны иметь разные рекомендации. Стиль struct msdn рекомендует использовать только со структурами, которые представляют однородное значение, но если нужно представить некоторые независимые значения, скрепленные вместе изолентой, не следует использовать этот стиль структуры - следует использовать структуру с открытые публичные поля.
supercat
0

Я не знаю, но использовали ли вы когда-нибудь 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)
Бионический киборг
источник