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

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

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

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

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

Я использую .NET 3.5 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% быстрее , чем при использовании класса. Я не знаю, чем отличается мой трассировщик лучей.

Ответов (12)

Решение

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

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

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

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

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


структура

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

vvvv

класс

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

pppp

..в..в ... вв.

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

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

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

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

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

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

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

а также

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

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

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

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

в качестве

Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

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

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

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

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

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

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

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

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

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

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

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

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

  • Логически представляет одно значение
  • Имеет размер экземпляра менее 16 байт
  • Не будет изменен после создания
  • Не будет приведен к ссылочному типу