Как стать автором
Обновить

Комментарии 33

Мсье знает толк. И это хорошо.
НЛО прилетело и опубликовало эту надпись здесь
Асм листинг :) Но я решил его не вставлять сюда — это нужно далеко не всем, тем кому это действительно надо не составит труда самому заглянуть в листинг — ссылка на проект в самом низу. Плюс ко всему статья и так обьемная получилась, около 8-ми страниц А4.
НЛО прилетело и опубликовало эту надпись здесь
>почему ручной ассемблер такой неприлично медленный получился.
Компилятор лучше меня знает все о инструкциях — количество циклов, какие могут застоллить пайплайн, как лучше их перетасовать, что бы избежать столлов, как протолкнуть данные в регистры и из регистров — VFP и NEON разделяют между собой набор регистров. Вот и компилятор в асм листинге для интринсиков протаскивал некоторые данные через VFP регистры.

К тому же я не специалист по ассемблеру. Возможно, что кто-то более опытный, сразу заметит косяки в моем асме.

>Я думал, у вас под рукой есть
Тоже нету — я сейчас не за маком
Интересный тест.
Судя по результатам самым быстрым оказался GLKMath. Интересно было бы посмотреть какой код выдает компилятор при использовании GLKMatrix4Multiply.
Самым быстрым оказались интринсики. GLKMath — это просто узкоспециализировання библиотека с векторизированным кодом. Шаг в сторону — и прийдется писать самому. Я здесь её больше привел для сравнения и что бы показать, что новый Clang очень хорошо оптимизирует код. GCC 4.2 жутко сливал в этой задаче и на нем GLKMath давал крошечный прирост.
А как поведет себя cblas не смотрели? Я использовал его для несколько больших массивов, но здесь он тоже, кажется, может быть применим.
>А как поведет себя cblas не смотрели?
Нет. Я даже не в курсе, что это. Быстро погуглил, пробежался по коду — не заметил там никакой векторизации. Так что этот будет выполняться на FPU и даст соответствующий результат…
каждое ядро процессора снабжено своим NEON юнитом, когда же VFP — один на процессор.
Здрасьте, с чего это NEON-ов больше чем VFP?

Но у NEON’а есть одна киллер фича – он может параллельно обрабатывать 4 32-х битных флоата, в то время как PowerVR SGX – только один.
NEON в Cortex-A9 64х битный и умеет обрабатывать только ДВА * 32-х битных флоата параллельно.
C какого испуга USSE выполняет лишь 1 флоат операцию? Или речь идёт про пайп а не про ядро?

К сожалению, я не нашел в нем прифетча, что, видимо, и является причиной более медленного кода.
У A9 есть аппаратный префетч (на несколько стримов)

Здрасьте, с чего это NEON-ов больше чем VFP?
Моя ошибка :(

NEON в Cortex-A9 64х битный и умеет обрабатывать только ДВА * 32-х битных флоата параллельно.
128-ми битный. Пруф — infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0409e/Chdceejc.html

C какого испуга USSE выполняет лишь 1 флоат операцию? Или речь идёт про пайп а не про ядро?
USSE обрабатывает 32-х! битные флоаты на скалярном процессоре. Если флоаты 16-ти битные — тогда они выполняются на векторном движке. Это описано где-то в мануалах от Imagination. В Rogue этот недостаток будет исправлен. На этом делается акцент в спецификации OpenGL ES 3.0

У A9 есть аппаратный префетч (на несколько стримов)
У А8, который как раз в моем iPod Touch 4, нету апаратного прифетча
128-ми битный
Не все инструкции 128битные

infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0409e/Chdceejc.html
VMUL Dd,Dn,Dm[x] 1 такт
VMUL Qd,Qn,Dm[x] 2 такта

обьяснение остальных значений — чуть выше в п. 3.4.1. Instruction timing tables

Krait и другие современные ARM процессоры — имеют полноразмерный NEON

USSE обрабатывает 32-х! битные флоаты на скалярном процессоре.
Если брать в расчёт только SGX535, то да.
SGX543 это следующее поколение — USSE2, который обрабатывает 2 x FP32
Табличка имеет разные GPU, соотвественно чтобы не создавать путаницы, лучше уточнять какой именно описывается.

Не все инструкции 128битные
Я сразу не уловил замечание про размер инструкций. Думал, разговор идет о размере SIMD регистров.
Замечание учтено. Спасибо.

SGX543 это следующее поколение — USSE2, который обрабатывает 2 x FP32
К сожалению мне не удалось найти какой либо официальной информации на этот счет. Потому взял профайлер шейдеров, посмотрел на количество циклов с 535-м компилятором и 543-м. Получилось одно и то же. С чего я и сделал соответствующий вывод. Если вы сможете дать достоверную, подтвержденную информацию на этот счет — буду очень благодарен. Пока я оставлю статью так, как она есть.
Если вы сможете дать достоверную, подтвержденную информацию на этот счет — буду очень благодарен

www.imgtec.com/powervr/sgx_series5XT.asp
USSE2 delivers twice the peak floating point and instruction throughput of Series5 USSE

en.wikipedia.org/wiki/PowerVR#Series_5_.28SGX.29
SGX535 2 pipes / 2 TMU
SGX543 4 pipes / 2 TMU

Зачем так делать? Зачем перечитывать матрицу из памяти?
Передавайте матрицу по значению. Компилятор не будет оптимизировать ваш асм код даже в случае инлайна.
inline void Matrix4ByVec4(float32x4x4_t* __restrict__ mat, const float32x4_t* __restrict__ vec, float32x4_t* __restrict__ result)
{
    asm
    (
       "vldmia %0, { d24-d31 } \n\t"
       "vld1.32    {q1}, [%1]\n\t"


Cortex A8 кстати поддерживает dual-issue для вычислителной инструкции и load-store
infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344i/BABHBCCB.html
Зачем перечитывать матрицу из памяти?
Я НЕ перечитываю матрицу из памяти. Я загружаю её в NEON регистры
 for (int j = 0; j < 4; ++j) {
            Matrix4ByVec4(&mvp, &squareVertices[j], &data[i + j].pos);
        }


А это что? 4 раза перечитываете то, что можно загрузить 1 раз.
Я не в курсе, какой ABI у iOS, но если уж рассказыаваете об оптимизации,
то использовать функции вида «умножить 1 вектор на матрицу» — не лучший подход.
Нужно умножать сразу пачку векторов, тогда можно будет выполнять чтение данных параллельно
вычислениям и не создавать пузыри в конвеере.

Сделал тест. Код:
__attribute__((always_inline)) void CalculateSpriteVertsWorldPos(const float32x4x4_t* __restrict__ mvp, float32x4_t* __restrict__ v1, float32x4_t* __restrict__ v2, float32x4_t* __restrict__ v3, float32x4_t* __restrict__ v4)
{
    __asm__ volatile
    (
     "vldmia %0, { q8-q11 }\n\t"
     "vldmia %1, { q0-q3 } \n\t"

     "vmul.f32 q12, q8, d0[0]\n\t"
     "vmla.f32 q12, q9, d0[1]\n\t"
     "vmla.f32 q12, q10, d1[0]\n\t"
     "vmla.f32 q12, q11, d1[1]\n\t"
     
     "vmul.f32 q13, q8, d2[0]\n\t"
     "vmla.f32 q13, q9, d2[1]\n\t"
     "vmla.f32 q13, q10, d3[0]\n\t"
     "vmla.f32 q13, q11, d3[1]\n\t"
     
     "vmul.f32 q14, q8, d4[0]\n\t"
     "vmla.f32 q14, q9, d4[1]\n\t"
     "vmla.f32 q14, q10, d5[0]\n\t"
     "vmla.f32 q14, q11, d5[1]\n\t"
     
     "vmul.f32 q15, q8, d6[0]\n\t"
     "vmla.f32 q15, q9, d6[1]\n\t"
     "vmla.f32 q15, q10, d7[0]\n\t"
     "vmla.f32 q15, q11, d7[1]\n\t"
     
     "vstmia %2, { q12 }\n\t"
     "vstmia %3, { q13 }\n\t"
     "vstmia %4, { q14 }\n\t"
     "vstmia %5, { q15 }"
     
     :
     : "r" (mvp), "r" (squareVertices), "r" (v1), "r" (v2), "r" (v3), "r" (v4)
     : "memory", "q0", "q1", "q2", "q3", "q8", "q9", "q10", "q11", "q12", "q13", "q14", "q15"
     );
}


Результат изменился не значительно — было 5300, стало 4780, прирост — около 10%. Пытался перетасовать стор инструкции — особой разницы не заметил.
НЛО прилетело и опубликовало эту надпись здесь
тогда зачем там opengl часть вообще, можно выкинуть её и нормально замерять время.
по-моему это не принципиально. В обеих случаях ОГЛ часть будет отнимать одинаковое количество времени.
НЛО прилетело и опубликовало эту надпись здесь
Во-первых вы создаёте очереди зависимых инструкций вместо того чтобы
использовать возможности конвеера.
У вас же столлы после каждой инструкции идут.

На A8:
«vmul.f32 q12, q8, d0[0]\n\t»
[ждём 5 тактов]
«vmla.f32 q12, q9, d0[1]\n\t»
[ждем 8 тактов]

«vmul.f32 q13, q8, d2[0]\n\t»
«vmla.f32 q13, q9, d2[1]\n\t»

=>

«vmul.f32 q12, q8, d0[0]\n\t»
«vmul.f32 q13, q8, d2[0]\n\t»
«vmul.f32 q14, q8, d4[0]\n\t»
«vmul.f32 q15, q8, d6[0]\n\t»
[ждём 1 такт]

«vmla.f32 q12, q9, d0[1]\n\t»
«vmla.f32 q13, q9, d2[1]\n\t»

Во-вторых, я бы на вашем месте, раз уж если вы раскрыли цикл, обьединил эту функцию с Matrix4ByMatrix4(), так как они всегда выпоняются в последовательно в одинаковой конфигурации,
тем самым убрав операции соседние чтения / записи матрицы, также создающие столл.
Изменения не значительные. Результат — 4677
Ну значит тормозит не там, а например рандом() % N =)
Дизассемблер смотрели версии с интринсиками? Профайлинг что говорит? в iOS тулсете же есть профайлер?
Всё различие версии с интринсиками должно заключаться в переупорядочивании команд.
Рандом считается отдельно. Это можно на скриншоте с профайлера увидеть.

Дизассемблер смотрели версии с интринсиками?
Всё различие версии с интринсиками должно заключаться в переупорядочивании команд.

В этом-то и вся магия) Я не спец по ассемблеру, в отличии от ребят, которые писали Clang. По коду видно, что компилятор делает много хитрой работы — данные тянет напрямую в d\q регистры и через VFP (s регистры). Опять же у меня под рукой сейчас нет Мака. Да и суть статьи в другом — юзайте интринсики. Они без особых усилий помогут получить максимум производительности, все остальное сделает компилятор. Так же на них можно быстро строить быстрый код из готовых блоков:
        float32x4x4_t mvp;
        Matrix4ByMatrix4((float32x4x4_t*)proj.m, (float32x4x4_t*)modelviewMat.m, &mvp);
        
        for (int j = 0; j < 4; ++j) {
            Matrix4ByVec4(&mvp, &squareVertices[j], &data[i + j].pos);
        }

при условии, что соответствующие методы векторизированы.
Против написания кастомного кода под каждый конкретный случай:
__restrict__ mvp, float32x4_t* __restrict__ v1, float32x4_t* __restrict__ v2, float32x4_t* __restrict__ v3, float32x4_t* __restrict__ v4)
{
    __asm__ volatile
    (
     "vldmia %0, { q8-q11 }\n\t"
     "vldmia %1, { q0-q3 } \n\t"

     "vmul.f32 q12, q8, d0[0]\n\t"
     "vmla.f32 q12, q9, d0[1]\n\t"
     "vmla.f32 q12, q10, d1[0]\n\t"
     "vmla.f32 q12, q11, d1[1]\n\t"
     
     "vmul.f32 q13, q8, d2[0]\n\t"
     "vmla.f32 q13, q9, d2[1]\n\t"
     "vmla.f32 q13, q10, d3[0]\n\t"
     "vmla.f32 q13, q11, d3[1]\n\t"
     
     "vmul.f32 q14, q8, d4[0]\n\t"
     "vmla.f32 q14, q9, d4[1]\n\t"
     "vmla.f32 q14, q10, d5[0]\n\t"
     "vmla.f32 q14, q11, d5[1]\n\t"
     
     "vmul.f32 q15, q8, d6[0]\n\t"
     "vmla.f32 q15, q9, d6[1]\n\t"
     "vmla.f32 q15, q10, d7[0]\n\t"
     "vmla.f32 q15, q11, d7[1]\n\t"
     
     "vstmia %2, { q12 }\n\t"
     "vstmia %3, { q13 }\n\t"
     "vstmia %4, { q14 }\n\t"
     "vstmia %5, { q15 }"
     
     :
     : "r" (mvp), "r" (squareVertices), "r" (v1), "r" (v2), "r" (v3), "r" (v4)
     : "memory", "q0", "q1", "q2", "q3", "q8", "q9", "q10", "q11", "q12", "q13", "q14", "q15"
     );
}

в который еще никто, кроме авторе не вникнет. Ну и для кроссплатформенности это еще один минус. У Эпиков, к примеру, в коде вообще нет асма, только интринсики вместо него.
С таймингами немного нагнал я, но посыл должен быть ясен.

infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344k/BCGDCECC.html
У инструкции VMLA значение аккумулятора должно быть готово на 3-м такте выполнения.
Результат VMUL готов через 5 тактов. Т.е. «первый» столл, по-идее, получается не 5, а 2 такта.

Органичения VMLA:

If a VMLA.F is followed by an VADD.F or VMUL.F with no RAW hazard, the VADD.F or VMUL.F stalls 4 cycles before issue. The 4 cycle stall preserves the in-order retirement of the instructions.

Т.е.
vmla.f32 q14, ...
[4 такта столл даже без Read-After-Write зависимостей]
vmul.f32 q15, ...

A VMLA.F followed by any NEON floating-point instruction with RAW hazard stalls for 8 cycles.
В случае «второго» столла, последовательность зависимых VMLA / VMLA могла бы выполнится быстрее если бы не это ограничение.
Меня тоже очень интересует почему ассемблер настолько медленнее — в моём случае это было +40%
Жду теста с использованием уже загруженной матрицы.
Дело в дата лэйауте. У меня помимо позиции на каждую вершину лежит и цвет. То есть прийдется писать специальную ф-цию для этого случая. Опять же, хорошее замечание. Я постараюсь в ближайшее время изменить код и посмотреть на результат. Соответственно, проапдейчу пост
Всё таки не увидели в конце результата — на сколько это даёт прироста по филрейту.
На сколько раельные сцены можно считать на CPU.

Мы некогда делали гонки под iPhone 3g ещё.
Там после всех отсечений скармливалось видяхе 20к поликов.
У вас обсчёт их обсчёт (считаем спрайт как два треугольника) сьел 18-20% CPU.
Но сегодня бы я не стал делать в 3д игре 20к поликов — т.к. это выглядит довольно деревянно, по современным меркам.
А увеличить кол-во поликов — и процессор будет только этим и занят, а надо ещё физику считать, и ещё отсекать от огромной сцены обьекты, чтоб не кормить видяхе лишнее, иначе она тоже захлебнётся, иногда успевать декодировать музыку, считать звук и прочее.

Вот физику посчитать на неоне — это наверно самое то было бы.
Во-первых — это демо, а не реальный игровой проект.

У вас обсчёт их обсчёт (считаем спрайт как два треугольника) сьел 18-20% CPU.
Тогда попытайтесь нарисовать 10к динамичных спрайтов любым другим способом, к примеру по draw call'у на спрайт — боюсь, что в таком случае ваш фпс просядет раз в 10-20…

Всё таки не увидели в конце результата — на сколько это даёт прироста по филрейту.
Ровно на столько, на сколько мы освободили USSE от вершинного процессинга. Здесь замкнутый круг — если я буду сравнивать свой, так сказать, софтварный инстансинг с draw call'ом на спрайт — то я получу громадную разницу, но сравнение будет не объективным, так как при таком инстансинге я упираюсь именно в филлрейт, а при ДИПе на спрайт — в синхронизацию процессора с гпу. Так же, к сожалению, я не могу померить использование USSE на iOS девайсе — что бы знать, на сколько я освободил его от расчетов вершинных шейдеров. Это можно сделать имея не залоченное железо на Андроиде, Линуксе или Винде.

А увеличить кол-во поликов — и процессор будет только этим и занят, а надо ещё физику считать, и ещё отсекать от огромной сцены обьекты
Ну так современные девайсы имеют больше одного ядра — зачем же им простаивать-то?!
Если игра 2д с кучей спрайтов, которые каждый кадр меняют своё положение — то да ваш способ действительно имеет смысл.
У меня и мысли не было что кому то в голову придёт скармливать видяхе по одному спрайту.
2д считаем на cpu, складываем в буфер и пачкой засылаем в GPU.

Но если игра 3д — то там же нет необходимости по паре полигонов гонять — засунули большой меш в GPU, оно его переварило и нарисовало.

Наверно я не совсем правильно воспринял, для чего всё это вы задумали. Пример со спрайтами должен был это подсказать :)
Все не ограничивается спрайтами. К примеру, если у тебя требование — OpenGL ES 1.1, то скининг мешей так же лучше делать на НЕОНе. Если множество низко полигональных объектов — их так же будет быстрее софтварно заинстансить, чем рисовать по одному.
Вобщем выигрыш просматривается тогда, когда нет возможности скормить ограниченное количество мешей, с большим количеством треугольников, а надо мелкими порциями скармливать, когда вызовы к драйверу просто всё убьют.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации