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

95

Я собираюсь создать в коде 100 000 объектов. Они маленькие, всего 2 или 3 объекта. Я помещаю их в общий список, а когда они есть, я зацикливаю их, проверяю значение aи, возможно, обновляю значение b.

Быстрее / лучше создавать эти объекты как класс или как структуру?

РЕДАКТИРОВАТЬ

а. Свойства являются типами значений (кроме строки, как мне кажется?)

б. Они могут (мы еще не уверены) иметь метод проверки

РЕДАКТИРОВАТЬ 2

Мне было интересно: объекты в куче и стеке обрабатываются сборщиком мусора одинаково или это работает по-разному?

Мишель
источник
2
У них будут только общедоступные поля или у них также будут методы? Являются ли типы примитивными типами, например целыми числами? Будут ли они содержаться в массиве или в чем-то вроде List <T>?
ДжеффФергюсон
14
Список изменяемых структур? Остерегайтесь велоцираптора.
Энтони Пеграм
1
@Anthony: Боюсь, мне не хватает шутки про велоцирапторов: -s
Мишель
5
Шутка про велоцирапторов взята из XKCD. Но когда вы говорите о том, что `` типы значений выделяются в стеке '', неправильное представление / детали реализации (удалите, если применимо), тогда вам нужно остерегаться Эрика Липперта ...
Грег Бич

Ответы:

138

Является ли это быстрее , чтобы создавать эти объекты как класс или как структуры?

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

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

Что лучше: создавать эти объекты как класс или как структуру?

Может быть, класс, может быть, структура. Практическое правило: если объект:
1. Маленький
2. Логически неизменное значение
3. Их много,
тогда я бы подумал о том, чтобы сделать его структурой. В противном случае я бы остановился на ссылочном типе.

Если вам нужно изменить какое-либо поле структуры, обычно лучше создать конструктор, который возвращает целую новую структуру с правильно установленным полем. Возможно, это немного медленнее (измерьте!), Но логически гораздо легче рассуждать.

Обрабатываются ли объекты в куче и стеке сборщиком мусора одинаково?

Нет , они не совпадают, потому что объекты в стеке являются корнями коллекции . Сборщику мусора даже не нужно спрашивать, «жива ли эта штука в стеке?» потому что ответ на этот вопрос всегда: «Да, это в стеке». (Теперь вы не можете полагаться на это, чтобы поддерживать объект в живых, потому что стек - это деталь реализации. Джиттеру разрешено вводить оптимизацию, которая, скажем, регистрирует то, что обычно было бы значением стека, а затем никогда не в стеке Таким образом, GC не знает, что он все еще жив. Зарегистрированный объект может агрессивно собирать его потомков, как только регистр, хранящий его, больше не будет считываться.)

Но сборщик мусор действительно должен обработать объекты в стеке , как живые, так же, как он относится к какому - либо объекту , известный как живые , как живые. Объект в стеке может относиться к объектам, выделенным в куче, которые необходимо поддерживать в рабочем состоянии, поэтому GC должен обрабатывать объекты стека как живые объекты, выделенные в куче, для целей определения динамического набора. Но очевидно, что они не рассматриваются как «живые объекты» для уплотнения кучи, потому что они вообще не находятся в куче.

Это ясно?

Эрик Липперт
источник
Эрик, знаете ли вы, использует ли компилятор или джиттер неизменяемость (возможно, если принудительно readonly), чтобы обеспечить оптимизацию. Я бы не позволил этому повлиять на выбор изменчивости (в теории я помешан на деталях эффективности, но на практике мой первый шаг к эффективности всегда пытается получить как можно более простую гарантию правильности и, следовательно, не должен тратить циклы ЦП и мозговые циклы на проверки и крайние случаи, и в этом помогает соответствующая изменчивость или неизменность), но это противодействует любой резкой реакции на ваше высказывание о том, что неизменяемость может быть медленнее.
Джон Ханна,
@Jon: компилятор C # оптимизирует константные данные, но не данные только для чтения . Я не знаю, выполняет ли jit-компилятор какие-либо оптимизации кэширования для полей только для чтения.
Эрик Липперт,
Жаль, поскольку я знаю, что знание неизменяемости допускает некоторую оптимизацию, но на тот момент выходит за пределы моих теоретических знаний, но я бы хотел расширить их. Между тем, «это может быть быстрее в обоих направлениях, вот почему, теперь проверьте и выясните, что применимо в этом случае» полезно иметь возможность сказать :)
Джон Ханна
Я бы рекомендовал прочитать simple-talk.com/dotnet/.net-framework/… и вашу собственную статью (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/…, чтобы начать погружение в подробности. Есть много других хороших статей. Кстати, разница в обработке 100 000 небольших объектов в памяти едва ли заметна из-за некоторых накладных расходов памяти (~ 2,3 МБ) для класса. Это легко проверить простым тестом.
Ник Мартыщенко
23

Иногда structвам не нужно вызывать конструктор new () и напрямую назначать поля, что делает его намного быстрее, чем обычно.

Пример:

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);
}

где Value- structс двумя полями ( idи isValid).

struct Value
{
    int id;
    bool isValid;

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

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

Джон Алексиу
источник
Очевидно, что все становится намного быстрее, когда вы также переносите значения за пределы собственных границ.
leppie
Я бы предложил использовать другое имя list, учитывая, что указанный код не будет работать с расширением List<Value>.
supercat 04
7

Структуры могут показаться похожими на классы, но есть важные различия, о которых вам следует знать. Прежде всего, классы - это ссылочные типы, а структуры - это типы значений. Используя структуры, вы можете создавать объекты, которые ведут себя как встроенные типы, а также пользоваться их преимуществами.

Когда вы вызываете оператор New для класса, он будет размещен в куче. Однако когда вы создаете экземпляр структуры, она создается в стеке. Это даст прирост производительности. Кроме того, вы не будете иметь дело со ссылками на экземпляр структуры, как с классами. Вы будете работать напрямую с экземпляром структуры. Из-за этого при передаче структуры методу она передается по значению, а не по ссылке.

Подробнее здесь:

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

Kyndigs
источник
4
Я знаю, что это написано в MSDN, но MSDN не рассказывает всей истории. Стек против кучи - это деталь реализации, и структуры не всегда помещаются в стек. Только один недавний блог по этому поводу
Энтони Пеграм,
"... он передается по значению ..." и ссылки, и структуры передаются по значению (если не используется 'ref') - отличается ли передается значение или ссылка, то есть структуры передаются по значению , объекты класса передаются по ссылке, а параметры с пометкой ref передаются по ссылке.
Пол Руан
10
Эта статья вводит в заблуждение по нескольким ключевым моментам, и я попросил команду MSDN отредактировать или удалить ее.
Эрик Липперт
2
@supercat: чтобы обратиться к вашему первому пункту: важнее то, что в управляемом коде, где хранится значение или ссылка на значение, в значительной степени не имеет значения . Мы упорно трудились , чтобы сделать модель памяти , которая большую часть времени позволяет разработчикам , чтобы среда выполнения принимать умные решения для хранения от их имени. Эти различия имеют очень большое значение, когда непонимание их приводит к катастрофическим последствиям, как в C; не так много в C #.
Эрик Липперт,
1
@supercat: чтобы ответить на ваш второй вопрос, никакие изменяемые структуры в основном не являются злом. Например, void M () {S s = new S (); s.Blah (); N (с); }. Выполните рефакторинг: void DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Это только что привело к ошибке, потому что S является изменяемой структурой. Вы сразу заметили ошибку? Или тот факт, что S является изменяемая структура скрывает от вас ошибку?
Эрик Липперт,
6

Массивы структур представлены в куче в непрерывном блоке памяти, тогда как массив объектов представлен как непрерывный блок ссылок с самими фактическими объектами в другом месте в куче, что требует памяти как для объектов, так и для их ссылок на массивы. .

В этом случае, поскольку вы помещаете их в List<>List<>поддерживается массивом), было бы более эффективно с точки зрения памяти использовать структуры.

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

Пол Руан
источник
Вы можете использовать refключевое слово, чтобы справиться с этим.
leppie
«Однако помните, что большие массивы попадут в кучу больших объектов, где, если их время жизни велико, могут отрицательно повлиять на управление памятью вашего процесса». - Я не совсем понимаю, почему вы так думаете? Выделение на LOH не вызовет каких-либо неблагоприятных последствий для управления памятью, если (возможно) это недолговечный объект, и вы хотите быстро освободить память, не дожидаясь коллекции Gen 2.
Джон Артус,
@Jon Artus: LOH не уплотняется. Любой долгоживущий объект разделит LOH на область свободной памяти до и после. Для выделения требуется непрерывная память, и если эти области недостаточно велики для выделения, для LOH выделяется больше памяти (т.е. вы получите фрагментацию LOH).
Paul Ruane
4

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

Что касается вашего второго редактирования, сборщик мусора имеет дело только с кучей, но места в куче намного больше, чем пространства стека, поэтому размещение вещей в стеке не всегда является победой. Кроме того, список типов структур и список типов классов в любом случае будет в куче, так что в данном случае это не имеет значения.

Редактировать:

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

Могут быть разумные исключения с частными вложенными структурами, где все использования этой структуры, следовательно, ограничены очень ограниченной областью. Однако здесь это не применяется.

На самом деле, я думаю, что «он мутирует, так что это плохой трюк» не намного лучше, чем говорить о куче и стеке (что, по крайней мере, оказывает некоторое влияние на производительность, даже если оно часто искажается). «Он видоизменяется, поэтому, скорее всего , нет смысла рассматривать его как имеющий семантику значений, поэтому это плохая структура» - это лишь немного отличается, но я думаю, что это важно.

Джон Ханна
источник
3

Лучшее решение - измерить, снова измерить, а затем измерить еще раз. Могут быть детали того, что вы делаете, что может затруднить упрощенный и легкий ответ, например «использовать структуры» или «использовать классы».

FMM
источник
согласен с частью меры, но, на мой взгляд, это был прямой и ясный пример, и я подумал, что, возможно, об этом можно было бы сказать что-то общее. И, как выяснилось, некоторым удалось.
Мишель
3

По сути, структура - это не что иное, как совокупность полей. В .NET структура может «притворяться» объектом, и для каждого типа структуры .NET неявно определяет тип объекта кучи с теми же полями и методами, которые, будучи объектом кучи, будут вести себя как объект . Переменная, которая содержит ссылку на такой объект кучи ("упакованная" структура), будет демонстрировать семантику ссылки, но переменная, которая содержит непосредственно структуру, представляет собой просто агрегирование переменных.

Я думаю, что путаница между структурами и классами во многом проистекает из того факта, что у структур есть два очень разных варианта использования, которые должны иметь очень разные рекомендации по проектированию, но рекомендации 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

Используйте классы.

По общему правилу. Почему бы не обновлять значение b по мере их создания?

Прит Сангха
источник
1

С точки зрения C ++ я согласен с тем, что изменение свойств структуры будет медленнее по сравнению с классом. Но я думаю, что их будет быстрее читать из-за того, что структура выделяется в стеке, а не в куче. Чтение данных из кучи требует больше проверок, чем из стека.

Роберт
источник
0

Что ж, если вы пойдете с struct afterall, то избавьтесь от строки и используйте char или байтовый буфер фиксированного размера.

Это re: производительность.

Даниэль Мошмондор
источник