Потоки в Unity3D — измеряем производительность

Привет!
Иногда меня спрашивают об отложенных действиях или о многопоточности в Unity3D и обычно я советую обратить внимание на класс Coroutine, благо он позволяет решать большинство повседневных задач, связанных с задержками и проч.
Однако, иногда при разработке действительно требуется настоящая многопоточность, при поиске пути, например.
Поэтому я решил сделать на скорую руку небольшой тест, в котором можно было бы пощупать многопоточность и сравнить производительность исполнения кода в одном (главном) потоке и в многопоточной реализации. Для начала я поискал какие-нибудь менеджеры потоков, чтобы не велосипедить и наткнулся на простецкий (но в моем случае, удобный) класс Loom от whydoidoit.

Далее я по-быстрому слепил приложение, скомпилировал его под разные платформы (в архиве в конце статьи есть бинарники для всех основных платформ и сорцы самого проекта) и посмотрел на результаты. . Некоторые мне показались интересными, поэтому я решим ими поделиться, может быть кому-то это будет полезно.

Вообще код, который пытался нагружать CPU прост как пробка — я просто пробегаюсь по приличному массиву экземпляров Vector3D (10 000 000 элементов под десктопами и 1 000 000 под мобилами):

private void Run()
{
const float scale = 432.654f;

for (int j = 0; j < arrayLength; j++)
{
Vector3 v = vertices[j];

v = Vector3.Lerp(v * scale / 123f * v.magnitude, v / scale * 0.0123f * v.magnitude, v.magnitude);

vertices[j] = v;
}
}

Как видите, «начинка» цикла по сути ничего конкретного не делает — просто случайный набор всяких медленных вещей)
Так же я добавил на сцену модельку муравья (привет создателям примеров для Away3D! :)), которая крутится каждый Update(), дабы можно было сразу увидеть — когда приложение подвисает, «задумываясь», а когда — нет.

JTMLjQ0

Как я ранее сказал, я провел некоторые тесты на подручном оборудовании, проверял скорость работы как в синхронном режиме (выполнение в главном потоке), так, и в асинхронном (массив бьется на столько равных частей, сколько ядер доступно приложению и каждая часть обрабатывается в отдельном потоке), и получил я такие результаты (S — синхр, A — асинхр):

PC (Intel Core i7 2600K 4.6Ghz, 4-core + Hyper Threading)

Standalone PC build (Fastest quality, 1024×768 windowed)
S: 920.8 мс
A: 198.2 мс (8 потоков, x4.64)

WebPlayer build (Chrome 25.0.1364.172, Unity WebPlayer Release Channel Plugin v. 4.1.1f)
S: 938.1 мс
A: 200.4 мс (8 потоков, x4.68)

PC (Intel Core i7 Q740 1.73Ghz, 4-core + Hyper Threading)

Standalone PC build (Fastest quality, 1024×768 windowed)
S: 1732.1 мс
A: 584.4 мс (8 потоков, x2.96)

WebPlayer build (Chrome 25.0.1364.172, Unity WebPlayer Release Channel Plugin v. 4.1.1f)
S: 1780.0 мс
A: 555.3 мс (8 потоков, x3.21)

PC (Intel Atom N455 1.66GHz 1-Core + Hyper Threading)

Standalone PC build (Fastest quality, 1024×600 windowed)
S: 7835.3 мс
A: 6533.9 мс (2 потока, x1.20)

WebPlayer build (Chrome 25.0.1364.172, Unity WebPlayer Release Channel Plugin v. 4.1.1f)
S: 7701.6 мс
A: 6778.3 мс (2 потока, x1.14)

Mobile

Galaxy Tab 10.1 (1.4GHz OC 2-core CPU, Tegra 2, Android 4.0.4, GT-P7510)
S: 1479.0 мс
A: 831.1 мс (2 потока, x1.78)

iPad 1 (1Ghz 1-core CPU)
S: 5364.0 мс
A: 6865.7 мс (1 поток, x0.78)

Не дурно, да? На платформах с двух- и более ядерными CPU прирост производительности может достигать хороших показателей (до x4.68 по сравнению с исполнением в главном потоке в нашем случае!), в целом, на всех устройствах кроме первого iPad производительность «расчетов» так или иначе возросла при использовании многопоточности.
Во всех случаях тест асинхронного выполнения кода не вызывал зависания приложения или другого дискомфорта, за исключением теста на Intel Atom, где просел FPS. Ему простительно, он старый и одноядерный 🙂

Обратите внимание на разницу в производительности двух разных поколений Intel Core i7 — оба имеют 4 ядра с HT на борту, однако более свежее поколение демонстрирует гораздо бОльшую эффективность применения многопоточности, что конечно хорошо.
Также весьма интересные получились результаты тестов на камне Intel Atom — у него всего 1 ядро, но зато есть HT, которая и позволила несколько улучшить производительность кода в многопоточном исполнении, мелочь, а приятно!)
Кстати, следуем заметить, что хоть и «проседает» FPS при тестировании этого Атома, приложение все ещё остается отзывчивым.

Результаты тестов на мобильных платформах тоже интересные.
Galaxy Tab 10.1 показал себя с лучшей стороны, показав почти двукратный прирост скорости выполнения асинхронного теста по сравнению с синхронным, при этом уверенно обогнав первый iPad по скорости выполнения обоих тестов, хотя, думаю тут помог разгон до 1.4 ГГц =P
Первый iPad имеет одноядерный CPU, поэтому дополнительные потоки он не сумеет обработать на HW уровне, вот почему выполнение нашего кода в отдельном потоке тут медленней. Ведь, по сути мы заставляем его работать над расчетами и обработкой сцены одновременно, переключаясь то в главный поток, то обратно в наш дочерний. Если бы на сцене было что-то посущественней, то думаю FPS бы просел. С другой стороны, асинхронное выполнение кода позволяет избежать зависания приложения, иногда это даже более важная задача, чем сама скорость расчетов (и вот тут кстати обычно справляются Coroutine’ы).

Кстати, приятно, что потоки работают на мобильных платформах — они могут быть отличным подспорьем Coroutin’ам!

А теперь посмотрим на сравнение общего времени выполнения всех проведенных тестов:

chart1
Общая разница не очень большая… Но некоторый прирост в производительности все же есть + мы получаем работу нашего кода без зависания приложения, не так уж плохо!

Теперь оставим в сравнении только железо с двух- и более ядерными CPU:
chart2
Ого! А вот теперь разница хорошо заметна. Так что можно смело использовать потоки на математически сложных или очень объемных алгоритмах — вы не только получите плавную работу приложения во время просчётов, но и неслабый прирост производительности на многоядерных устройствах!

Можно встроить простую проверку в код — если

SystemInfo.processorCount

будет возвращать 2 или более — можно смело переключаться на многопоточное исполнение сложного алгоритма.

Некоторые из вас наверняка заметили, что я не проводил тестов на добре, которое можно получить с помощью Flash Exporterа. Причина проста — потоки не поддерживаются (пока?) экспортером. Я полагаю, что задача слишком не тривиальна — ведь настоящие потоки во флэше возможны лишь при использовании Workers, а их концепция слишком далека от таковой в .NET, в общем, сделать это очень трудно, если вообще возможно.

Не забудьте, что дополнительные потоки в вашем мобильном приложении могут быстро убить батарею на устройствах с многоядерными процессорами — будьте осторожны!
И, как верно заметил DbIMok в комментариях, при работе с потоками — помните, настоящие потоки не будут работать с объектами Unity-движка (кроме некоторых исключений, например Debug.Log будет работать и из отдельных потоков), в то время как coroutine’ы будут, т.к. работают по принципу «green threads» — исполняют код в течение нескольких «кадров», но в основном потоке.

Думаю, пока это все, вы можете скачать архив со скомпилированными примерами и исходниками проекта тут (~27МБ).
Это стандартный LZMA2 архив, сжатый в 7-zip. Используйте 7-zip 9.x или аналог для распаковки.

PS: И да, я буду рад услышать от вас любые идеи или замечания по данной теме, а так же было бы интересно получить результаты тестов на других устройствах и железе!

Комментарии

Потоки в Unity3D — измеряем производительность — 3 комментария

  1. Нужно упомянуть отсутствие у потоков, отличных от основного, доступа к объектам UnityEngine. То есть чтобы читатель понял: корутины в основном потоке, «размазывают» выполнение между кадрами http://www.richardfine.co.uk/junk/unity%20lifetime.png http://www.richardfine.co.uk/junk/unity%20lifetime%20poster.png а потоки ограничены задачами, не влияющими напрямую на объекты движка.

    • Привет! Спасибо за комментарий, полезное замечание, совсем забыл об этом упомянуть!

    • Я посмотрел этот класс, там ведь есть QueueOnMainThread поставить очередь в главный поток, а из главного потока уже юзать объекты…
      Помойму всё отлично работает.
      Спасибо за статью