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

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

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

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

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

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

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

EDIT: Как было предложено в некоторых ответах, я создал тестовый проект, в котором я попытался передать структуры как 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 unused. 
}

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

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

Ответы

Ответ 1

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

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

Ответ 2

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

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


структура

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

VVVV

класс

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

PPPP

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

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

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

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

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

и

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

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

Ответ 11

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

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

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

Ответ 12

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

а

Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

где со структурой является nullable

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

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