Вопрос

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

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

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

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

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

Так что в основном:есть какие-нибудь советы, что следует учитывать и чего следует избегать?

Редактировать:Как предлагалось в некоторых ответах, я создал тестовый проект, в котором я попытался передавать структуры как 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 для любого оптимального способа передачи этой структуры?

РЕДАКТИРОВАТЬ 2:Кстати, я должен отметить, что использование структур в моем тестовом проекте является примерно на 50% быстрее, чем при использовании класса.Почему это отличается для моего raytracer, я не знаю.

Это было полезно?

Решение

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

При передаче большего объема памяти это неизбежно приведет к перегрузке кэша.

Другие советы

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

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


структура

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

ввввв

класс

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

пппп

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

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

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

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

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

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

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

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

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

и

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

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

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

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

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

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

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

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

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

Мой собственный трассировщик лучей также использует struct Vectors (хотя и не Rays), и изменение Vector на class, по-видимому, не оказывает никакого влияния на производительность.В настоящее время я использую три дубля для вектора, так что он может быть больше, чем должен быть.Однако следует отметить одну вещь, и это может быть очевидно, но это было не для меня, а именно запустить программу за пределами visual Studio.Даже если вы установите для него оптимизированную сборку выпуска, вы можете получить значительное увеличение скорости, если запустите exe-файл вне VS.Любой бенчмаркинг, который вы проводите, должен принимать это во внимание.

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

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

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

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

как

Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

где со структурой обнуляется

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

Но вы (очевидно) потеряете все свои функции наследования

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top