Когда составляется ответ?
Я занимаюсь хобби-проектом raytracer, и изначально я использовал структуры для своих объектов Vector и Ray, и я подумал, что raytracer - идеальная ситуация для их использования: вы создаете миллионы из них, они живут не дольше, чем один метод, они легкие. Однако, просто изменив "struct" на "class" в Vector и Ray, я получил очень значительное повышение производительности.
Что дает? Они оба маленькие (3 поплавка для вектора, 2 вектора для луча), не копируются слишком много. Конечно, я передаю их методам, когда это необходимо, но это неизбежно. Итак, каковы распространенные подводные камни, которые снижают производительность при использовании структур? Я прочитал эту статью MSDN, в которой говорится следующее:
Запустив этот пример, вы увидите, что цикл структуры на несколько порядков быстрее. Однако важно остерегаться использования ValueTypes, когда вы рассматриваете их как объекты. Это добавляет дополнительные расходы на упаковку и распаковку вашей программы и может в конечном итоге стоить вам дороже, чем если бы вы застряли с объектами! Чтобы увидеть это в действии, измените код выше, чтобы использовать массив foos и bars. Вы обнаружите, что производительность более или менее равна.
Однако он довольно старый (2001 год), и все "помещение их в массив вызывает коробку / распаковку" показалось мне странным. Это правда? Однако я предварительно рассчитал первичные лучи и поместил их в массив, поэтому я взялся за эту статью и вычислил первичный луч, когда мне это было нужно, и никогда не добавлял их в массив, но он ничего не изменил: классы, это было все еще в 1,5 раза быстрее.
Я использую.NET 3.5 с пакетом обновления 1 (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, я не знаю.
12 ответов
По сути, не делайте их слишком большими, и передавайте их по ссылке, когда можете. Я обнаружил это точно так же... Изменив классы Vector и Ray на структуры.
С увеличением объема передаваемой памяти это неизбежно вызывает перегрузку кеша.
Массив структур будет представлять собой единую непрерывную структуру в памяти, в то время как элементы в массиве объектов (экземпляры ссылочных типов) должны быть индивидуально адресованы указателем (т. Е. Ссылкой на объект в куче, собираемой мусором). Поэтому, если вы обращаетесь к большим коллекциям элементов одновременно, структуры дадут вам выигрыш в производительности, поскольку им нужно меньше косвенных ссылок. Кроме того, структуры не могут быть унаследованы, что может позволить компилятору выполнить дополнительную оптимизацию (но это только возможность и зависит от компилятора).
Однако структуры имеют совершенно другую семантику присваивания и также не могут быть унаследованы. Поэтому я обычно избегал бы структур, за исключением указанных причин производительности, когда это необходимо.
структура
Массив значений v, закодированный структурой (типом значения), выглядит следующим образом в памяти:
VVVV
учебный класс
Массив значений v, закодированный классом (ссылочным типом), выглядит следующим образом:
PPPP
..v..v... ст.
где p - это указатели или ссылки this, которые указывают на фактические значения v в куче. Точки указывают другие объекты, которые могут быть перемежены в куче. В случае ссылочных типов вам нужно ссылаться на v через соответствующий p, в случае типов значений вы можете получить значение напрямую через его смещение в массиве.
В рекомендациях о том, когда использовать структуру, говорится, что она не должна быть больше 16 байт. Ваш вектор составляет 12 байтов, что близко к пределу. Луч имеет два вектора, что составляет 24 байта, что явно превышает рекомендованный предел.
Когда структура становится больше 16 байт, ее невозможно более эффективно скопировать с помощью одного набора инструкций, вместо этого используется цикл. Таким образом, проходя этот "магический" лимит, вы фактически делаете гораздо больше работы, когда передаете структуру, чем когда вы передаете ссылку на объект. Вот почему код работает быстрее с классами, хотя при размещении объектов возникает больше накладных расходов.
Вектор все еще может быть структурой, но Луч просто слишком велик, чтобы работать как структура.
Все, что написано о боксе / распаковке до.NET-дженериков, может быть воспринято с некоторой долей соли. Универсальные типы коллекций избавили от необходимости упаковывать и распаковывать типы значений, что делает использование структур в этих ситуациях более ценным.
Что касается вашего конкретного замедления - нам, вероятно, нужно увидеть код.
Я думаю, что ключ кроется в этих двух утверждениях из вашего поста:
вы создаете миллионы из них
а также
Я передаю их методам, когда это необходимо, конечно
Теперь, если ваша структура меньше или равна 4 байтам (или 8 байтам в 64-битной системе), вы копируете гораздо больше при каждом вызове метода, чем если вы просто передали ссылку на объект.
Первое, что я хотел бы найти, это убедиться, что вы явно реализовали Equals и GetHashCode. Невыполнение этого означает, что реализация каждого из них во время выполнения выполняет очень дорогие операции для сравнения двух экземпляров структуры (внутренне она использует отражение для определения каждого из приватных полей, а затем проверяет их на равенство, что приводит к значительному выделению),
Тем не менее, в общем, лучшее, что вы можете сделать, это запустить свой код под профилировщиком и посмотреть, где находятся медленные части. Это может быть откровением.
Вы профилировали приложение? Профилирование - это единственный верный способ узнать, где именно проблема производительности. Существуют операции, которые, как правило, лучше / хуже для структур, но если вы не профилируете, вы просто догадаетесь, в чем проблема.
Хотя функциональность схожа, структуры обычно более эффективны, чем классы. Вы должны определить структуру, а не класс, если тип будет работать лучше в качестве типа значения, чем ссылочного типа.
В частности, типы конструкций должны соответствовать всем этим критериям:
- Логически представляет одно значение
- Имеет размер экземпляра менее 16 байт
- Не будет изменено после создания
- Не будет приведен к ссылочному типу
Мой собственный трассировщик лучей также использует структуру Векторы (но не Лучи), и изменение вектора на класс не оказывает никакого влияния на производительность. В настоящее время я использую три двойника для вектора, поэтому он может быть больше, чем должен быть. Следует отметить одну вещь, и это может быть очевидно, но это было не для меня, а это запуск программы за пределами Visual Studio. Даже если вы установите оптимизированную сборку релиза, вы сможете значительно увеличить скорость, если запустите exe вне VS. Любой бенчмаркинг, который вы делаете, должен учитывать это.
Я использую структуры в основном для параметров объектов, возвращая несколько частей информации из функции, и... ничего больше. Не знаю, правильно это или неправильно, но это то, что я делаю.
Если структуры малы и их не слишком много, то СЛЕДУЕТ помещать их в стек (если это локальная переменная, а не член класса), а не в кучу, это означает, что GC не ' Это должно быть вызвано, и распределение / освобождение памяти должно быть почти мгновенным.
При передаче структуры в качестве параметра в функцию структура копируется, что означает не только большее количество выделений / освобождений (из стека, которое почти мгновенно, но все еще имеет издержки), но и издержки при простой передаче данных между двумя копиями, Если вы передаете по ссылке, это не проблема, так как вы просто указываете, откуда читать данные, а не копируете их.
Я не уверен на 100% в этом, но я подозреваю, что возврат массивов с помощью параметра 'out' также может дать вам повышение скорости, так как память в стеке зарезервирована для него и не должна копироваться как стек "разматывается" в конце вызова функции.
Вы также можете превратить структуры в обнуляемые объекты. Пользовательские классы не смогут создавать
как
Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>
где со структурой обнуляется
Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>
Но вы (очевидно) потеряете все свои наследственные свойства