Когда составляется ответ? - PullRequest
32 голосов
/ 28 февраля 2009

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

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

Ответы [ 12 ]

26 голосов
/ 28 февраля 2009

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

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


* структура 1007 *

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

VVVV

класс

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

PPPP

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

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

11 голосов
/ 28 февраля 2009

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

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

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

9 голосов
/ 28 февраля 2009

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

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

6 голосов
/ 28 февраля 2009

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

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

6 голосов
/ 28 февраля 2009

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

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

и

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

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

6 голосов
/ 28 февраля 2009

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

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

4 голосов
/ 28 февраля 2009

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

2 голосов
/ 17 апреля 2009

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

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

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

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

0 голосов
/ 28 февраля 2009

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

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

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

...