Структури проти класів


93

Я збираюся створити 100 000 об’єктів у коді. Вони невеликі, лише з 2 або 3 властивостями. Я покладу їх у загальний список, і коли вони є, я оберну їх циклом і перевірте значення aта, можливо, оновіть значення b.

Чи швидше / краще створити ці об’єкти як клас чи як структура?

EDIT

а. Властивості - цінні типи (крім рядка, на який я думаю?)

b. Вони можуть (ми ще не впевнені) мати метод підтвердження

РЕДАГУВАТИ 2

Мені було цікаво: чи предмети на купі та групі обробляються однаково сміттєзбірником, чи це працює по-різному?


2
Вони матимуть лише загальнодоступні поля, або вони також матимуть методи? Чи є типи примітивних типів, таких як цілі числа? Чи будуть вони міститись у масиві чи в чомусь на зразок List <T>?
JeffFerguson

14
Список змінних структур? Слідкуйте за велоцираптором.
Ентоні Пеграм

1
@Anthony: Я боюся, що я пропускаю жарт велоцираптора: -s
Мішель

5
Анекдот з велоцираптором - з XKCD. Але коли ви кидаєте навколо "типи значень, виділені на стек", детально помилкове уявлення / реалізація (видаліть, якщо це можливо), то це Ерік Ліпперт, на який потрібно стежити ...
Грег Бук,

Відповіді:


137

Чи швидше створювати ці об’єкти як клас чи як структура?

Ви єдина людина, яка може визначити відповідь на це запитання. Спробуйте обидва способи, виміряйте значущий, орієнтований на користувача відповідний показник ефективності, і тоді ви дізнаєтесь, чи зміна має значущий вплив на реальних користувачів у відповідних сценаріях.

Структури споживають менше купи пам’яті (тому що вони менші та легше ущільнюються, а не тому, що вони «на стеку»). Але для копіювання потрібно більше часу, ніж копія. Я не знаю, які показники вашої ефективності для використання або швидкості пам’яті; тут є компроміс, і ти людина, яка знає, що це.

Чи краще створити ці об’єкти як клас чи як структура?

Можливо клас, а може і структура. Як правило, якщо об'єкт:
1. Малий
2. Логічно незмінне значення
3. Їх дуже багато,
то я б роздумав зробити його структурою. Інакше я б дотримувався посилального типу.

Якщо вам потрібно мутувати якесь поле структури, зазвичай краще побудувати конструктор, який повертає цілу нову структуру з правильно встановленим полем. Це, можливо, трохи повільніше (виміряйте це!), Але логічно набагато простіше міркувати.

Чи рівномірно обробляються сміттєзбірники предмети на купі та на стопі?

Ні , вони не однакові, тому що предмети на стеці є корінням колекції . Збиральнику сміття не потрібно ніколи запитувати "чи ця річ на стопі жива?" тому що відповідь на це питання завжди "Так, це на стеці". (Зараз на це не можна покладатися, щоб зберегти об’єкт живим, оскільки стек є деталлю реалізації. Джиттеру дозволяється вводити оптимізації, які, скажімо, реєструють те, що зазвичай було б значенням стека, і тоді воно ніколи не знаходиться в стеку тож GC не знає, що він ще живий. Зареєстрований об’єкт може зібрати своїх нащадків агресивно, як тільки реєстр, що тримається на ньому, не буде прочитаний знову.)

Але збирач сміття дійсно повинен обробити об'єкти в стеку , як живі, так само, як він ставиться до будь - якого об'єкта , відомий як живі , як живі. Об'єкт на стеку може посилатися на виділені з купи об'єкти, які потрібно підтримувати в живих, тому для визначення цілісного набору GC повинен обробляти об'єкти стеків, як об'єкти, виділені з живої купи. Але очевидно, що вони не розглядаються як "живі об'єкти" для цілей ущільнення купи, тому що вони перебувають не в купі.

Це зрозуміло?


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

@Jon: Компілятор C # оптимізує дані про const, але не лише для читання даних. Я не знаю, чи виконує компілятор jit які-небудь оптимізації кешування на полях для читання.
Ерік Ліпперт

Шкода, як я знаю, знання про незмінність дозволяє здійснити певні оптимізації, але в цей момент вдарив межі моїх теоретичних знань, але це межі, які я хотів би розтягнути. Тим часом "це може бути швидше обома способами. Ось чому, тепер тестуйте та з’ясуйте, що застосовується в даному випадку" корисно, щоб можна було сказати :)
Джон Ханна

Я рекомендую прочитати simple-talk.com/dotnet/.net-framework/… та власну статтю (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/…, щоб почати пірнати в деталі. Навколо багато інших хороших статей. До речі, різниця в обробці 100 000 малих об'єктів в пам'яті навряд чи помітна через деяку накладну пам'ять (~ 2,3 Мб) для класу. Це можна легко перевірити простим тестом.
Нік Мартищенко

Так, це зрозуміло. Щиро дякуємо за ваш вичерпний (кращий обширний? Google translate дав 2 переклади. Я мав на увазі сказати, що ви знайшли час, щоб не написати коротку відповідь, але знайшли час і написати всі деталі) відповіді.
Мішель

23

Іноді structвам не потрібно викликати новий () конструктор і безпосередньо призначати поля, що робить його набагато швидшим, ніж зазвичай.

Приклад:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

приблизно в 2-3 рази швидше, ніж

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

де Valuea structз двома полями ( idі isValid).

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

З іншого боку, елементи повинні бути переміщені або вибрані типи значень, все, що копіювання буде уповільнювати. Щоб отримати точну відповідь, я підозрюю, що вам доведеться профілювати свій код і перевірити його.


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

Я б запропонував використовувати інше ім’я, окрім того list, що вказаний код не працюватиме з a List<Value>.
supercat

7

Структури можуть здатися схожими на класи, але є важливі відмінності, про які слід пам’ятати. Перш за все, класи - це еталонні типи, а структури - цінні типи. Використовуючи структури, ви можете створювати об’єкти, які ведуть себе як вбудовані типи і користуються також їх перевагами.

Коли ви зателефонуєте Новому оператору на клас, він буде розподілений на купі. Однак, коли ви створюєте інстанцію структуру, вона створюється на стеці. Це призведе до підвищення продуктивності. Крім того, ви не будете мати справу з посиланнями на екземпляр структури, як у класі. Ви будете працювати безпосередньо з екземпляром struct. Через це при передачі структури методу він передається за значенням, а не як еталон.

Більше тут:

http://msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx


4
Я знаю, це говорить про MSDN, але MSDN не розповідає всієї історії. Стек проти купи - це деталь реалізації, і структури не завжди йдуть у стеку. Лише один останній блог про це дивіться: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Ентоні

"... це передається за значенням ..." і посилання, і структури передаються за значенням (якщо не використовується "ref") - це чи передається значення або посилання, яке відрізняється, тобто структури передаються за значенням за значенням , об'єкти класу передаються посиланням за значенням, а параметри, позначені параметри ref, передають посилання-посилання.
Paul Ruane,

10
Ця стаття вводить в оману кілька ключових моментів, і я попросив команду MSDN переглянути або видалити її.
Ерік Ліпперт,

2
@supercat: для вирішення вашої першої точки: більша точка полягає в тому, що в керованому коді, де зберігається значення або посилання на значення, в основному не має значення . Ми наполегливо працювали над створенням моделі пам’яті, яка більшість часу дозволяє розробникам дозволяти виконувати рішення від розумного зберігання від їх імені. Ці відмінності мають велике значення, коли нерозуміння їх спричиняє катастрофічні наслідки, як це відбувається в C; не так багато в C #.
Eric Lippert,

1
@supercat: щоб вирішити вашу другу точку, жодні незмінні структури в основному не є злими. Наприклад, недійсний M () {S s = new S (); s.Blah (); N (s); }. Refactor на: недійсний DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Щойно ввели помилку, оскільки S - це мутаційна структура. Ви відразу побачили помилку? Або зробив факт, що S є змінна структура приховає від вас помилку?
Ерік Ліпперт

6

Масиви структур представлені на купі у суміжному блоці пам’яті, тоді як масив об’єктів представлений як суміжний блок посилань із власне власними об’єктами в іншому місці в купі, таким чином, потрібна пам'ять як для об’єктів, так і для їх посилань на масив .

У цьому випадку, оскільки ви розміщуєте їх у List<>(і a List<>підтримується масивом), було б ефективніше використовувати пам’яті з урахуванням пам’яті.

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


Ви можете використовувати refключове слово для вирішення цього питання.
леппі

"Однак остерігайтеся, що великі масиви знайдуть свій шлях на великій купі об'єктів, де, якщо їхній термін служби тривалий, може негативно вплинути на управління пам'яттю вашого процесу." - Я не зовсім впевнений, чому ви так подумаєте? Виділення на LOH не спричинить негативних наслідків для управління пам'яттю, якщо (можливо) це недовговічний об'єкт, і ви хочете швидко відновити пам'ять, не чекаючи колекції Gen 2.
Джон Артус

@ Джон Артус: ЛОГ не ущільнюється. Будь-який довгоживучий об’єкт розділить LOH на область вільної пам’яті до та на область після. Для розподілу потрібна суцільна пам'ять, і якщо ці області недостатньо великі для виділення, то для LOH виділяється більше пам'яті (тобто ви отримаєте фрагментацію LOH).
Пол Руан

4

Якщо вони мають значення значення семантики, то, ймовірно, ви повинні використовувати структуру. Якщо вони мають референтну семантику, то, ймовірно, слід використовувати клас. Є винятки, які переважно схиляються до створення класу, навіть якщо є значення семантики, але починаються звідти.

Що стосується Вашого другого редагування, GC має справу лише з купою, але місця нагромадження набагато більше, ніж простору стека, тому розміщення речей у стеці не завжди є виграшем. Окрім цього, список типів структур і список типів класів буде в купі в будь-якому випадку, тому це не має значення в цьому випадку.

Редагувати:

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

Можуть бути розумні винятки з приватними вкладеними структурами, де всі використання цієї структури обмежені в дуже обмеженій області. Тут це не стосується.

Дійсно, я думаю, що "це мутує, так що це погана штукатурка" не набагато краще, ніж продовжувати купу і стек (що, принаймні, має певний вплив на продуктивність, навіть якщо це часто неправильне представлення). "Це мутує, тому, цілком імовірно , не має сенсу вважати, що воно має семантику цінностей, тому це погана структура", лише трохи відрізняється, але що важливо, я думаю.


3

Найкраще рішення - виміряти, виміряти ще раз, потім виміряти ще. Можливо, є деталі того, що ви робите, що може ускладнити спрощену, просту відповідь на зразок "використовувати структури" або "використовувати класи".


згоден з частиною міри, але, на мою думку, це був прямий і чіткий приклад, і я думав, що, можливо, про це можна сказати якісь загальні речі. І як виявилося, деякі люди це зробили.
Мішель

3

По суті структура - це не більше і не менше, ніж сукупність полів. У .NET структура може "прикидатися" об'єктом, і для кожного типу структури .NET неявно визначає тип об'єкта купи з тими ж полями і методами, які - будучи об'єктом купи - будуть вести себе як об'єкт . Змінна, яка містить посилання на такий об'єкт купи ("boxed" структура), буде демонструвати референтну семантику, але та, яка містить структуру безпосередньо, - це просто агрегація змінних.

Я думаю, що велика плутанина між структурами та класом випливає з того, що в структурах є два дуже різні випадки використання, які повинні мати дуже різні керівні принципи проектування, але інструкції щодо MS не розрізняють їх. Іноді виникає потреба в чомусь, що поводиться як предмет; у такому випадку вказівки щодо MS є досить розумними, хоча, "обмеження 16 байтів", мабуть, має більше нагадувати 24-32. Однак іноді потрібно лише об’єднання змінних. Структура, яка використовується для цієї мети, повинна просто складатися з масиву загальнодоступних полів, і, можливо, Equalsперевизначення, ToStringпереопределення таIEquatable(itsType).Equalsвпровадження. Структури, які використовуються як агрегація полів, не є об'єктами і не повинні претендувати на їх. З точки зору структури, значення поля повинно бути не більш-менш, ніж "останнє, що написано в це поле". Будь-яке додаткове значення повинно визначатися кодом клієнта.

Наприклад, якщо структура, що агрегує змінну, має члени Minimumі Maximum, сама структура не повинна це обіцяти Minimum <= Maximum. Код, який отримує таку структуру як параметр, повинен вести себе так, ніби він переданий окремо Minimumі Maximumзначеннями. Вимога, яка Minimumне Maximumповинна бути більшою, ніж повинна розглядатися як вимога, щоб Minimumпараметр не був більшим, ніж окремо переданий Maximum.

Корисна модель, яку слід враховувати іноді, - це ExposedHolder<T>визначити клас таким чином:

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

Якщо є структура List<ExposedHolder<someStruct>>, де someStructструктура, що агрегує змінну, вона може робити такі речі myList[3].Value.someField += 7;, але myList[3].Valueякщо інший код надасть їй зміст, Valueа не надасть йому спосіб змінити його. На противагу цьому, якщо хтось використовував a List<someStruct>, його потрібно було б використовувати var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Якщо використовується тип мутаційного класу, для викриття вмісту myList[3]зовнішньому коду потрібно буде скопіювати всі поля до якогось іншого об'єкта. Якщо хтось використовував тип непорушного класу або структуру "об'єктного стилю", потрібно було б сконструювати новий екземпляр, який був подібний, myList[3]за винятком someFieldякого був іншим, а потім зберегти цей новий екземпляр у списку.

Ще одна примітка: Якщо ви зберігаєте велику кількість подібних речей, можливо, буде добре зберігати їх у можливо вкладених масивах структур, бажано намагаючись зберегти розмір кожного масиву між 1 К і 64 К або більше. Масиви структур є особливими, оскільки індексація дає пряме посилання на структуру всередині, тому можна сказати "a [12] .x = 5;". Хоча можна визначити об’єкти, схожі на масив, C # не дозволяє їм спільно використовувати такий синтаксис з масивами.



1

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


1

Добре, якщо ви переходите до strufter afterall, то позбудьтеся рядка та використовуйте char із фіксованим розміром або байтовим буфером.

Ось що: продуктивність.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.