Когда дается ответ?

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

Что дает? Они оба маленькие (3 поплавка для вектора, 2 вектора для луча), не копируются чрезмерно. Я, конечно, передаю их методам, когда это необходимо, но это неизбежно. Итак, каковы общие подводные камни, которые убивают производительность при использовании структур? Я прочитал эту статью MSDN, в которой говорится следующее:

Когда вы запустите этот пример, вы увидите, что цикл структуры работает на несколько порядков быстрее. Однако важно остерегаться использования ValueTypes, когда вы относитесь к ним как к объектам. Это добавляет дополнительную нагрузку на упаковку и распаковку в вашу программу и может в конечном итоге обойтись вам дороже, чем если бы вы застряли с объектами! Чтобы увидеть это в действии, измените приведенный выше код, чтобы использовать массив foos и bars. Вы обнаружите, что производительность более или менее одинакова.

Однако он довольно старый (2001 г.), и вся фраза «размещение их в массиве вызывает упаковку / распаковку» показалась мне странной. Это правда? Однако я предварительно рассчитал первичные лучи и поместил их в массив, поэтому я занялся этой статьей и вычислил первичный луч, когда он мне нужен, и никогда не добавлял их в массив, но это ничего не меняло: с классов, он все еще был в 1,5 раза быстрее.

Я использую .NET 3.5 с пакетом обновления 1 (SP1), который, как я считаю, устранил проблему, из-за которой методы структуры никогда не были встроены, так что этого тоже не может быть.

Итак, в основном: какие советы, что нужно учитывать и чего избегать?

РЕДАКТИРОВАТЬ: Как было предложено в некоторых ответах, я создал тестовый проект, в котором я пробовал передавать структуры как ref. Способы добавления двух векторов:

public static VectorStruct Add(VectorStruct v1, VectorStruct v2)
{
  return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

public static VectorStruct Add(ref VectorStruct v1, ref VectorStruct v2)
{
  return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

public static void Add(ref VectorStruct v1, ref VectorStruct v2, out VectorStruct v3)
{
  v3 = new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

Для каждого из них я получил вариант следующего метода тестирования:

VectorStruct StructTest()
{
  Stopwatch sw = new Stopwatch();
  sw.Start();
  var v2 = new VectorStruct(0, 0, 0);
  for (int i = 0; i < 100000000; i++)
  {
    var v0 = new VectorStruct(i, i, i);
    var v1 = new VectorStruct(i, i, i);
    v2 = VectorStruct.Add(ref v0, ref v1);
  }
  sw.Stop();
  Console.WriteLine(sw.Elapsed.ToString());
  return v2; // To make sure v2 doesn't get optimized away because it's unused. 
}

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

EDIT2: кстати, я должен отметить, что использование структур в моем тестовом проекте примерно на 50% быстрее, чем использование класса. Я не знаю, чем отличается мой трассировщик лучей.


person JulianR    schedule 28.02.2009    source источник
comment
Удачи с проектом, трассировщик лучей - это то, чем я скоро займусь.   -  person GurdeepS    schedule 28.02.2009
comment
См. Также stackoverflow.com/questions/ 521298 / when-to-use-struct-in-c / (особенно там мой ответ :))   -  person Brian    schedule 28.02.2009
comment
Создание трассировщика лучей - это очень весело. Я нахожу захватывающим, что вы можете создать изображение из ничего, кроме набора плавающих элементов и относительно простой векторной математики.   -  person JulianR    schedule 28.02.2009
comment
Я не думаю, что в статье говорилось, что размещение структур в массиве вызывает бокс. Он предупредил, что их использование в местах, где ожидаются объекты, действительно вызывает бокс; например, если вы передадите их методу, ожидающему аргумента типа object.   -  person ILoveFortran    schedule 28.02.2009
comment
возможный дубликат Когда использовать структуру в C #?   -  person nawfal    schedule 27.05.2013


Ответы (12)


В принципе, не делайте их слишком большими и передавайте их по ссылке, когда можете. Я обнаружил это точно так же ... Изменив мои классы Vector и Ray на структуры.

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

person TraumaPony    schedule 28.02.2009
comment
Я думал, что типы значений помещаются в коробку, когда вы передаете их по ссылке? - person Erik Forbes; 28.02.2009
comment
Нет, совсем нет. Я полагаю, он просто передает указатель. Для небольших методов, например сложение и простые тесты пересечения лучей, стоимость разыменования указателя меньше, чем стоимость копирования памяти. - person TraumaPony; 28.02.2009
comment
Интересно, что переход по ссылке убьет всю мою красивую перегрузку операторов и элегантный код. Я попробую протестировать это. - person JulianR; 28.02.2009
comment
Переходя по ссылке? Суть структуры в том, что она настолько мала, что ее более эффективно копировать, чем иметь накладные расходы на ссылку на нее ... - person Guffa; 28.02.2009
comment
Судя по моему опыту, передача их в качестве рефери замедляла работу. - person Joan Venge; 28.02.2009
comment
Некромантинг, поскольку я стал немного более опытным с тех пор, как спросил об этом. При переписывании трассировщика лучей передача всех структур по ссылке действительно была намного быстрее. Но да, это действительно сделало мой код очень уродливым. Небольшие математические однострочные операторы превратились в 5-строчные блоки Vector3.Add / Subtract / Multiply / и т. Д., И мне пришлось прибегнуть к множеству общедоступных полей, потому что свойство не может быть передано по ссылке (имеет смысл, это метод после все). Теперь это действительно быстро, частота кадров в реальном времени в очень простых сценах. - person JulianR; 29.06.2009
comment
@JulianR: Во многих случаях правильным способом перемещения информации является передача структур по ссылке. Если можно избежать передачи или копирования структур по значению в любом случае, когда не требуется независимая копия, большие структуры могут работать так же хорошо, как и маленькие. Единственная трудность с этим подходом состоит в том, что в некоторых контекстах система будет настаивать на создании защитных копий структур (например, при вызове любого метода-члена для структуры в месте хранения readonly). Классы могут иметь преимущества в некоторых контекстах, но когда эти преимущества не нужны, структуры, переданные по ссылке, выигрывают. - person supercat; 11.03.2012
comment
@ErikForbes: структуры упаковываются, когда они хранятся как автономные объекты кучи, которые должны продолжать существовать вне контекста кода или объекта, который их использует. Каждый автономный объект кучи должен иметь информацию о типе, хранящуюся вместе с ним, поскольку не может быть ничего другого, определяющего его тип. Напротив, если у вас, например, переменная типа Rectangle, код, использующий эту переменную, будет знать, что это Rectangle; аналогично, если в классе есть поле этого типа, код, который использует это поле, будет знать, что это за тип. Передача в код структуры типа List<int>.Enumerator ... - person supercat; 09.08.2012
comment
... который ожидает, что IEnumerator<int> поместит его в коробку, потому что код кода, который использует перечислитель, в противном случае не имел бы никакого способа узнать, что ему было дано List<int>.Enumerator. Если бы вместо этого можно было написать такой метод, как void UseEnumerator<T>(ref T theEnumerator) where T:IEnumerator<int>, и вызвать его с List<int>.Enumerator, система сгенерирует специальную версию UseEnumerator<List<int>.Enumerator(), которая будет знать, что ее параметр имеет именно этот тип; поскольку код будет знать точный тип, упаковка не потребуется. - person supercat; 09.08.2012

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

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


структура

Массив значений v, закодированный структурой (типом значения), выглядит в памяти следующим образом:

vvvv

класс

Массив значений v, закодированный классом (ссылочным типом), выглядит так:

pppp

..v..v...v.v..

где p - указатели this или ссылки, которые указывают на фактические значения v в куче. Точки указывают на другие объекты, которые могут быть разбросаны по куче. В случае ссылочных типов вам нужно ссылаться на v через соответствующий p, в случае типов значений вы можете получить значение напрямую через его смещение в массиве.

person ILoveFortran    schedule 28.02.2009
comment
Кроме того, помимо излишнего косвенного обращения, линейный обход vvvv удобен для кеширования, а линейный обход ..v..v ... v.v .. - нет. Современное оборудование выполняет запросы к памяти по 64 байта. Даже если вы хотите загрузить 8 байтов из некоторого определенного места в памяти, 64 байта будут переданы и кэшированы в ЦП, то есть это будет всего 8/64 = 0,125 полезной нагрузки. - person Evgeny Panasyuk; 19.06.2013
comment
У вас есть источник массивов структур, работающих так, как вы описываете? (Я знаю, что это типы значений, но, возможно, в массиве хранятся ссылки на эти типы значений?) - person ispiro; 28.06.2018

В рекомендациях о том, когда использовать структуру, говорится, что она не должна быть больше 16 байт. Ваш вектор составляет 12 байтов, что близко к пределу. Луч имеет два вектора, размер которых составляет 24 байта, что явно превышает рекомендуемый предел.

Когда структура становится больше 16 байт, ее больше нельзя эффективно скопировать с помощью одного набора инструкций, вместо этого используется цикл. Таким образом, передавая этот «магический» предел, вы на самом деле выполняете гораздо больше работы при передаче структуры, чем при передаче ссылки на объект. Вот почему код с классами работает быстрее, хотя при выделении объектов больше накладных расходов.

Вектор все еще может быть структурой, но Луч слишком велик, чтобы хорошо работать как структура.

person Guffa    schedule 28.02.2009
comment
Я вижу, не знал, что ограничение в 16 байт было таким жестким пределом, думал об этом больше как о руководстве. Однако забавно то, что использование моего Ray в качестве структуры не сильно замедляет приложение, если вообще замедляет его, в то время как создание моего вектора структурой действительно замедляет его на 50%. - person JulianR; 28.02.2009
comment
Имея Vector как класс и Ray как структуру, Ray будет содержать две ссылки. Это будет работать одинаково, но вы можете получить некоторые удивительные семантические эффекты. Создание обеих структур - вот что выводит структуру Ray за пределы ограничения по размеру. - person Guffa; 28.02.2009
comment
Обработка структур оптимизирована для случая, когда длина структуры составляет 16 байтов или меньше; Таким образом, производительность 17-байтовой структуры будет заметно хуже, чем у 16-байтовой структуры. С другой стороны, если избегать передачи структур по значению (вместо этого, по возможности, передавать ref), даже 100-байтовая структура может работать лучше, чем 100-байтовый класс. - person supercat; 11.03.2012
comment
@supercat: Ну, передача структуры по ссылке будет работать примерно так же, как передача класса по значению. Если вы действительно используете данные в scruct / class, класс будет работать немного лучше из-за дополнительной косвенности, которую добавляет передача по ссылке. Кроме того, если вам нужно везде передавать структуру по ссылке, вы вынуждены писать довольно уродливый код. - person Guffa; 11.03.2012
comment
@Guffa: косвенное обращение существует независимо от того, используется ли он класс или структура. Если для каждого элемента есть отдельный экземпляр класса, производительность будет почти одинаковой, за исключением времени, необходимого для создания объектов класса, и дополнительных 12-24 байтов, необходимых для ссылки на класс и служебных данных объекта. Основное отличие состоит в том, что если я скажу someProc(ref myStruct);, процедура сможет изменять myStruct только до тех пор, пока не вернется. Напротив, если изменяемый объект класса когда-либо подвергается воздействию внешнего кода, невозможно узнать, когда этот код может вызвать его изменение. - person supercat; 12.03.2012

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

Что касается вашего конкретного замедления - нам, вероятно, понадобится код.

person Erik Forbes    schedule 28.02.2009
comment
Придумал что-то подобное, но разве массивы определенного типа не всегда были универсальными? Или был в int [] объект [] внутри .NET 1.0? Что касается исходного кода: я вряд ли могу опубликовать здесь весь исходный код, но я посмотрю, смогу ли я откопать что-нибудь подходящее. - person JulianR; 28.02.2009
comment
Да, массивы всегда были (своего рода) универсальными. - person configurator; 28.02.2009

Я думаю, что ключ кроется в этих двух утверждениях из вашего сообщения:

вы создаете миллионы из них

и

Я передаю их методам, когда это необходимо, конечно

Теперь, если размер вашей структуры меньше или равен 4 байтам (или 8 байтам, если вы используете 64-разрядную систему), вы копируете гораздо больше при каждом вызове метода, чем если бы вы просто передали ссылку на объект.

person Andrew Hare    schedule 28.02.2009
comment
Это все еще быстрее, чем огромная сборка мусора, которая могла бы произойти ... - person TraumaPony; 28.02.2009
comment
Но действительно ли он выполняет сборку мусора? Если я визуализирую действительно большое изображение, использование памяти моим процессом будет расти бесконечно, возможно, потому что даже при использовании 3 ГБ у меня все еще остается много памяти, и, возможно, в этом случае GC будет ждать, пока я не закончу загружать ПРОЦЕССОР. - person JulianR; 28.02.2009
comment
Что ж, это должен быть сборщик мусора. По крайней мере, это была проблема с моим трассировщиком лучей. - person TraumaPony; 28.02.2009
comment
Я согласен с TraumaPony. У меня была такая же проблема с созданным мной средством 3D-рендеринга. Когда что-то используется и уничтожается в одно мгновение, структуры действительно ускоряют работу. Также, если вы посмотрите, например, XNA, почти все, что я использовал, было структурой. - person Joan Venge; 28.02.2009
comment
Это верно только в том случае, если вы не передаете эти структуры другим методам. - person Andrew Hare; 28.02.2009
comment
Попробуйте передать эти структуры по ссылке и посмотрите, что произойдет. - person NM.; 28.02.2009
comment
Профилируйте приложение с помощью .NET Memory Profiler - person Andrei Rînea; 04.03.2009
comment
Если вы создаете миллионы отдельных экземпляров, структура будет предлагать лучшую производительность, чем неизменяемый класс. Почти всегда. Единственное, временные классы могут выиграть, если большинство ссылок указывают на экземпляры, которые могут использоваться другими ссылками. Наличие миллиона ссылок, которые все относятся к одному из трех неизменяемых экземпляров класса, вероятно, лучше, чем наличие миллиона структур, которые все «случайно» содержат одну из трех комбинаций полей, но если бы все миллионы ссылок указывали бы на экземпляры разностных классов, ничего достигается за счет использования класса, а не структуры. - person supercat; 11.03.2012

Первое, что я хотел бы найти, это убедиться, что вы явно реализовали Equals и GetHashCode. Неспособность сделать это означает, что реализация каждого из них во время выполнения выполняет очень дорогостоящие операции для сравнения двух экземпляров структуры (внутренне он использует отражение для определения каждого из частных полей, а затем проверяет их на равенство, это вызывает значительный объем выделения) .

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

person Community    schedule 28.02.2009
comment
Я попробовал переопределить, хотя я не использовал никаких сравнений векторов или лучей, но это не дало результата. Хороший совет, с этого момента я обязательно переопределю Equals и GetHashCode :) - person JulianR; 28.02.2009

Вы профилировали приложение? Профилирование - единственный верный способ увидеть, где на самом деле проблема с производительностью. Есть операции, которые обычно лучше / хуже для структур, но если вы не профилируете, вы просто будете гадать, в чем проблема.

person JaredPar    schedule 28.02.2009

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

В частности, типы структур должны соответствовать всем этим критериям:

  • Логически представляет собой одно значение
  • Имеет размер экземпляра менее 16 байт
  • Не будет изменен после создания
  • Не будет приведен к ссылочному типу
person Gineer    schedule 17.04.2009
comment
Я не согласен с ненавистью Эрика Липперта к изменяемым структурам. Конечно, определенные ограничения в дизайне .net делают изменяемые структуры менее дружественными, чем они были бы в противном случае, но массивы изменяемых структур часто являются правильным способом хранения вещей. На 64-битной машине массив из миллиона 8-байтовых структур займет 8 мегабайт ОЗУ; массив из миллиона экземпляров класса с 8-байтовыми полями займет 40 мегабайт. Даже если структура хранит 40 байт данных (что намного превышает рекомендуемый порог в 16 байт), она все равно сократит использование памяти на 50%. - person supercat; 20.01.2011

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

person Instance Hunter    schedule 28.02.2009

Мой собственный трассировщик лучей также использует структурные векторы (но не лучи), и изменение вектора на класс, похоже, не влияет на производительность. В настоящее время я использую три двойных вектора для вектора, поэтому он может быть больше, чем должен быть. Следует отметить одну вещь, и это может быть очевидно, но это было не для меня, а именно: запускать программу за пределами Visual Studio. Даже если вы настроите его на оптимизированную сборку выпуска, вы можете получить значительный прирост скорости, если запустите exe за пределами VS. Это следует учитывать при любом тестировании производительности.

person Morten Christiansen    schedule 17.04.2009
comment
Из любопытства, ваш векторный шрифт прозрачный или непрозрачный? Структуры открытого поля часто работают намного лучше, чем непрозрачные. Написание myVec.x = expr1; myVec.y = expr2; myVec.z = expr3; может занять столько же времени, сколько и просто настройка параметров для вызова конструктора. Сам вызов и любое время, потраченное в конструкторе, будут представлять собой чистую потраченную впустую накладные расходы. Точно так же, хотя бывают случаи, когда JITter может оптимизировать чтение свойства myPoint.x, которое просто считывает вспомогательное поле _x, во многих случаях JITter этого не делает. - person supercat; 13.01.2013
comment
Насколько я понимаю, некоторые 32-разрядные версии .net (по крайней мере, 2.0 и 3.x) были намного лучше при оптимизации доступа к свойствам класса, чем доступ к свойствам структуры, поэтому переключение с непрозрачных структур могло быть не намного быстрее, чем классы, даже при том, что производительность прозрачных структур сдует производительность классов или непрозрачных структур. - person supercat; 13.01.2013
comment
Мои векторы доступны только для чтения, настраиваются через конструктор. Я провел много тестов различных реализаций и не смог найти ничего быстрее, чем это, но я не могу вспомнить, тестировал ли я с прозрачными структурами. В .NET 4 я думаю, что это было так, они сделали некоторые оптимизации для структур и встраивания, которые помогли с некоторыми из упомянутых вами проблем. Кроме того, с тех пор, как я написал свой ответ, я изменил лучи на структуры для повышения производительности примерно на 10%. - person Morten Christiansen; 05.02.2013
comment
Мне было бы любопытно узнать, какую производительность вы получите, если перейдете от непрозрачных структур к прозрачным. Проведенный мной бенчмаркинг показывает, что иногда различия небольшие, но иногда могут быть довольно большими. - person supercat; 05.02.2013

Если структуры небольшие и их не так много одновременно, СЛЕДУЕТ размещать их в стеке (если это локальная переменная, а не член класса), а не в куче, это означает, что сборщик мусора не t должен быть вызван, а выделение / освобождение памяти должно происходить практически мгновенно.

При передаче структуры в качестве параметра функции структура копируется, что означает не только больше выделений / освобождений (из стека, что почти мгновенно, но все еще имеет накладные расходы), но и накладные расходы при простой передаче данных между двумя копиями. . Если вы передаете ссылку, это не проблема, поскольку вы просто указываете ей, откуда читать данные, а не копируете их.

Я не уверен в этом на 100%, но подозреваю, что возвращение массивов с помощью параметра out также может дать вам прирост скорости, поскольку память в стеке зарезервирована для него и ее не нужно копировать как стек. "раскручивается" в конце вызова функции.

person Grant Peters    schedule 28.02.2009
comment
Я часто слышу, что структуры находятся в стеке, а не в куче со сборкой мусора; это не совсем правильно. Они не живут одни, но они определенно могут жить в куче как часть другого объекта ссылочного типа. - person ILoveFortran; 28.02.2009
comment
При передаче структуры через копию дальнейшего выделения не требуется - соответствующая сумма резервируется в стеке перед вызовом. Тем не менее, копирование по-прежнему должно происходить, потому что удорожает передача структур, а не выделение памяти. - person ILoveFortran; 28.02.2009
comment
Я сказал, что это ДОЛЖНО быть помещено туда при условии, что они являются локальной переменной (как было подробно описано в вопросе). Кроме того, память действительно должна быть выделена из стека, в основном это не проблема, но все же есть небольшие накладные расходы. - person Grant Peters; 01.03.2009

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

as

Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

где со структурой допускает значение NULL

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

Но вы (очевидно) потеряете все свои особенности наследования

person BozoJoe    schedule 28.02.2009
comment
Это просто оборачивает структуру в класс, вызывая тем самым сборщик мусора. Вам лучше просто изменить свою структуру на класс, так как это будет иметь тот же эффект и, вероятно, будет немного менее запутанным - person Grant Peters; 28.02.2009