Чому новий тип Tuple у .Net 4.0 є посилальним типом (класом), а не типом значення (struct)


89

Хтось знає відповідь та / або має думку щодо цього?

Оскільки кортежі, як правило, не дуже великі, я вважаю, що для них буде більше сенсу використовувати структури, ніж класи. Що ти кажеш?


1
Для тих, хто спотикається тут після 2016 р. У c # 7 та новіших версіях Tuple літерали належать до сімейства типів ValueTuple<...>. Див. Посилання на типи кортежів C #
Tamir Daniely

Відповіді:


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. Ці проблеми ніколи не виникали б, якби кортежі були представлені типами значень, а не посилальними типами для початку ...


2
@ Bent: Так, це саме те, що я роблю, коли натрапляю на корточки на гарячому шляху у F #. Було б непогано, якби вони надали в коробці .NET Framework і коробки, і в коробці, і в коробці ...
JD

18
Що стосується віртуальної відправки, я думаю, що ваша провина недоречна: Tuple<_,...,_>типи могли бути запечатані, і в такому випадку ніяка віртуальна відправка не потрібна, незважаючи на те, що це посилальні типи. Мені цікавіше, чому вони не запечатані, ніж чому вони є еталонними типами.
kvb

2
З мого тестування, для сценарію, коли кортеж буде сформовано в одній функції і повернуто до іншої функції, а потім більше ніколи не буде використано, структури відкритого поля, здається, пропонують чудову продуктивність для будь-якого елемента даних розміру, який не такий величезний, щоб дути стек. Незмінні класи є кращими лише в тому випадку, якщо посилання будуть передані навколо достатньо, щоб виправдати вартість їх побудови (чим більший елемент даних, тим менше їх потрібно передавати, щоб компроміс був їм на користь). Оскільки кортеж повинен представляти просто купу змінних, склеєних між собою, структура здається ідеальною.
суперкіт

2
"кортежі без упаковки розміром до 512 байт все одно можуть бути швидшими, ніж упаковані" - який це сценарій? Можливо, ви зможете розподілити структуру 512B швидше, ніж екземпляр класу, що містить 512B даних, але передача її буде більш ніж у 100 разів повільнішою (припускаючи x86). Щось я пропускаю?
Groo


45

Причина, швидше за все, тому, що лише менші кортежі мали б сенс як типи значень, оскільки вони мали б невеликий розмір пам'яті. Більші кортежі (тобто ті, що мають більше властивостей) насправді потерпають у продуктивності, оскільки вони будуть більшими за 16 байт.

Замість того, щоб деякі кортежі були типами значень, а інші - типовими посиланнями, і змушували розробників знати, які саме, я вважаю, люди в Microsoft думали, що зробити їх усі посилальні типи простішими.

Ах, підозри підтверджені! Будь ласка, дивіться Будівля Кортежу :

Перше головне рішення полягало в тому, чи слід розглядати кортежі як еталон або тип значення. Оскільки вони незмінні будь-коли, коли ви хочете змінити значення кортежу, вам доведеться створити нове. Якщо вони є еталонними типами, це означає, що може створюватися багато сміття, якщо ви змінюєте елементи кортежем в щільному циклі. Кортежі F # були еталонними типами, але команда відчувала, що вони можуть реалізувати покращення продуктивності, якщо два, а можливо, три кортежі елементів є типами цінностей. Деякі команди, які створили внутрішні кортежі, використовували значення замість посилальних типів, оскільки їх сценарії були дуже чутливими до створення великої кількості керованих об'єктів. Вони виявили, що використання типу значення дає їм кращу продуктивність. У нашому першому проекті специфікації кортежу, ми зберегли дво-, три- та чотириелементні кортежі як типи значень, а решта були еталонними типами. Однак під час зустрічі з дизайнерами, в якій взяли участь представники інших мов, було вирішено, що цей "роздвоєний" дизайн буде заплутаним через дещо різну семантику між двома типами. Визначено, що послідовність у поведінці та дизайні має вищий пріоритет, ніж потенційне підвищення продуктивності. На основі цього введення ми змінили дизайн таким чином, що всі кортежі є еталонними типами, хоча ми попросили команду F # провести деяке дослідження ефективності, щоб перевірити, чи не відбулося прискорення при використанні типу значення для деяких розмірів кортежів. Він мав хороший спосіб перевірити це, оскільки його компілятор, написаний на F #, був хорошим прикладом великої програми, яка використовувала кортежі в різних сценаріях. Врешті-решт, команда F # виявила, що вона не отримала покращення продуктивності, коли деякі кортежі були типами значень замість посилальних типів. Це змусило нас почуватись краще завдяки нашому рішенню використовувати еталонні типи для кортежу.


3
Чудова дискусія тут: blogs.msdn.com/bclteam/archive/2009/07/07/…
Кіт Адлер

Ага, я розумію. Я все ще трохи заплутаний, що типи значень тут нічого не означають на практиці: P
Бент Расмуссен

Я просто прочитав коментар про відсутність загальних інтерфейсів, і коли раніше розглядав код, це було саме інше, що мене вразило. Це справді зовсім не надихає, наскільки незвичними є типи Tuple. Але, я гадаю, ти завжди можеш зробити свій власний ... Синтаксична підтримка в C # все одно відсутня. І все-таки принаймні ... Тим не менше, використання дженериків та обмежень, які він має, все ще залишається обмеженим у .Net. Існує значний потенціал для дуже загальних дуже абстрактних бібліотек, але дженерики, ймовірно, потребують додаткових речей, таких як коваріантні типи повернення.
Bent Rasmussen

7
Ваше обмеження "16 байт" є фіктивним. Коли я протестував це на .NET 4, я виявив, що GC настільки повільний, що невкладені кортежі розміром до 512 байт все одно можуть бути швидшими. Я б також поставив під сумнів результати тестів Microsoft. Б'юсь об заклад, вони проігнорували паралелізм (компілятор F # не паралельний), і саме тому уникнення GC справді окупається, оскільки робоча станція .NET GC також не паралельна.
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 #!

Редагувати (07.10.2017)

Набори структур тепер повністю підтримуються наступним чином:

  • Вбудований у mscorlib (.NET> = 4.7) як System.ValueTuple
  • Доступний як NuGet для інших версій
  • Синтаксична підтримка в C #> = 7
  • Синтаксична підтримка в F #> = 4.1

2
Якщо уникнути непотрібного копіювання, структура відкритого поля будь-якого розміру буде ефективнішою, ніж незмінний клас того самого розміру, за винятком випадків, коли кожен екземпляр скопіюється достатньо разів, щоб вартість такого копіювання перевищила витрати на створення об'єкта купи ( беззбиткова кількість копій залежить від розміру об'єкта). Таке копіювання може бути неминучим , якщо один хоче - структуру , яка претендує бути незмінні, але Структури , яка призначена з'являтися в вигляді набору змінних (що структура є ) можна ефективно використовувати навіть тоді , коли вони величезні.
supercat

2
Може бути так, що F # не гарно грає з ідеєю проходження структур ref, або може не сподобатися той факт, що так звані "незмінні структури" не є, особливо в боксі. Дуже погано .net ніколи не реалізовував концепцію передачі параметрів примусовим виконанням const ref, оскільки в багатьох випадках така семантика є тим, що насправді потрібно.
supercat

1
До речі, я розглядаю амортизовану вартість ЖК як частину вартості розподілу об'єктів; якщо L0 GC буде необхідним після кожного мегабайта розподілів, тоді вартість виділення 64 байтів становить приблизно 1/16000 вартості L0 GC, плюс частка вартості будь-яких L1 або L2 GC, які стають необхідними як наслідком цього.
supercat

4
"Я думаю, що добуток ймовірності виникнення на потенційну шкоду буде набагато вищим із структурами, ніж зараз із класами". FWIW, я дуже рідко бачив кортежі кортежів у дикій природі і вважаю їх недоліком дизайну, але я дуже часто бачу, як люди борються з жахливою продуктивністю, коли використовують (ref) кортежі як ключі в a Dictionary, наприклад тут: stackoverflow.com/questions/5850243 /…
JD

3
@Jon Минуло два роки з того часу, як я написав цю відповідь, і тепер я погоджуюсь з вами, що було б кращим, якби принаймні 2- та 3-кортежі були структурами. У зв'язку з цим було запропоновано голосову пропозицію користувача F # . Питання має певну актуальність, оскільки останніми роками спостерігається масове зростання застосувань у галузі великих даних, кількісного фінансування та ігор.
Marc Sigrist

4

Для двох кортежів ви все ще можете використовувати KeyValuePair <TKey, TValue> з попередніх версій загальної системи типів. Це тип значення.

Незначним уточненням статті Метта Елліса було б те, що різниця у семантиці використання між посиланням та типами значень є лише "незначною", коли діє незмінність (що, звичайно, має місце тут). Тим не менше, я думаю, було б найкращим у дизайні BCL не вносити плутанину в тому, що Tuple переходить на еталонний тип на якомусь порозі.


Якщо значення буде використано один раз після його повернення, структура відкритого поля будь-якого розміру перевершить будь-який інший тип за умови, що воно не настільки жахливо величезне, щоб підірвати стек. Вартість побудови об’єкта класу буде відшкодована лише в тому випадку, якщо посилання закінчиться спільним використанням кілька разів. Бувають випадки, коли для загального гетерогенного типу з фіксованим розміром корисно бути класом, але бувають і інші випадки, коли структура була б кращою - навіть для "великих" речей.
supercat

Дякуємо, що додали це корисне правило. Однак я сподіваюся, що ви не зрозуміли моєї позиції: я наркоман цінності. ( stackoverflow.com/a/14277068 не повинен залишати сумнівів).
Гленн Слейден,

Типи значень є однією з чудових особливостей .net, але, на жаль, особа, яка написала msdn dox, не зрозуміла, що для них існує кілька випадків несуміжного використання, і що різні випадки використання повинні мати різні вказівки. Стиль struct msdn рекомендує використовувати лише зі структурами, що представляють однорідне значення, але якщо потрібно представити деякі незалежні значення, скріплені скотчем, не слід використовувати цей стиль struct - слід використовувати структуру з відкриті публічні поля.
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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.