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

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

Подтверждаю — не смог заставить clang сгенерировать похожий код. На O2 задействуется SSE, без оптимизации — просто вызов memset. Так что это какая-то проблема VС.
Хм, по ссылке что вы дали, movaps на некоторых процах отрабатывает быстрее…
Это если movaps не потребует переключения контекста SSE. Или его теперь принудительно меняют при переключении на другой процесс?
Переключение режимов SSE-AVX занимает около 70 тактов. При обнулении малого массива и переключении SSE-AVX на каждой итерации разница во времени выполнения будет отличаться в разы, чего не наблюдается.
Переключение контекста при исключении #NM выполняет операционная система. Это не 70 тактов.
Я боюсь спросить, что вы такое делаете, что знаете подробности таких деталей?
Я один из разработчиков ОС Колибри и как раз делал переключение контекста fpu/sse. Поэтому знаю как всё работает. Обычно применяют отложенное переключение контекста. При установленном в 1 бите TS регистра cr0 первая команда сопроцессора вызывает исключение 7(#NM). Ядро сбрасывает бит TS и производит переключение контекста сопроцессора. Поэтому сложно сказать сколько тактов на самом деле займёт команда. На мой взгляд применять simd нет смысла, если объём данных меньше пары килобайт. С учётом длинных регистров avx планка ещё выше. В принципе ядро может отслеживать количество исключений для каждого процесса, и если сопроцессор активно используется переключать контекст сразу. Не знаю, используется такая схема или нет.
Подождите. Речь не о FPU/SSE, а о SSE/AVX, при переключении которого не происходит никаких исключений. Просто fallback YMM->XMM реализован довольно костыльно, и требует теневого копирования регистров при смене набора команд. На это и уходит около 70 тактов — точное количество зависит от реального числа «физических» регистров.
А при чём здесь AVX? Первоначально речь шла о movaps которая к AVX не относится. Это команда SSE. А внутренняя кухня процессора при использовании разных наборов команд дело тёмное. Откуда эта информация про теневое копирование? И для чего? Регистры ymm удваивают xmm и совпадают по формату. Это же не альяс fpu/mmx.
При том, что пользовательский код может содержать инструкции AVX, а тут опа — обнуление переменной через SSE. Эта ситуация не такая уж и редкая, на первый взгляд. Вот здесь чуть подробнее:
https://software.intel.com/en-us/articles/intel-avx-state-transitions-migrating-sse-code-to-avx

Информация про теневое копирование следует из технических спецификаций. Причина этому — аппаратная реализация регистров (маппинг), операций с ним на низком уровне и упрощение архитектуры процессора (реализация AVX команд через существующие блоки SSE и отказ от блоков копирования) за счёт потери некоторой функциональности.
Ага. Уже разобрался. Это особенность двух близких микроархитектур Sandy Bridge и Ivy Bridge в отличие от исключения при установленном бите cr0.ts, которое всегда работает. Кроме того компиляторы стараются защитить программистов от выстрела в ногу и не генерируют sse и avx код в одном объектном файле.
Компилятор генерирует адекватный код ровно до того момента, как программист начинает эти инструкции использовать. Самая неприятная для компилятора ситуация — это когда программист начинает вручную использовать avx и делать в коде две ветки: legacy и avx.

Решение простое: просто генерить два комплекта бинарников, чтобы не допустить смешения инструкций.
А xor eax, eax
обнуляет весь rax?

Может это на случая, если по какой-то ну очень важной причине программист напишет так:


char buffer[32] = { 1 };

и хочет получить


1 0 0 0 0 ...

Хотя я не знаю, зачем это может понадобиться.

Честно говоря, первый раз, когда-то давным давно, написав
char buffer[32] = { 1 };

Я ожидал получить в результате то же самое, что и
memset(buffer, 1, 32);

Эх, старые добрые времена… Хотя если честно мне такой вариант до сих пор кажется логичней.
Как я понял, проблема актуальна только для типов, размер которых меньше чем sizeof(void*)? Т.е. для массива double всей этой свистопляски с выравниванием не будет в принципе и будет одна лишняя операция, если компилятор не догадается всё в один memset впихнуть?
SSE регистры имеют размер 16 байт, AVX — 32 байта. Их запись также должна быть выровнена.
SSE регистры не имеют никакого отношения к функции memset.
SSE регистры имеют прямое отношение к memset, т.к. компиляторы бывают умные и могут его соптимизировать до пары ассемблерных инструкций, не используя библиотечные вызовы. И даже просто выкинуть обнуление, если после инициализации присваиваются другие значения.
В статье речь о memset, а не про то, как компилятор может оптимизировать SSE инструкции. В контексте SSE память должна быть выровнена на 16 байт, с массивами вообще веселуха получается с «лишними» байтами в начале массива. Думаю, про такие специфичные случаи можно вообще не говорить.
Один movups, не требующий выравнивания, но работающий медленнее, часто выходит быстрее обычных операций. В моих проектах (обработка изображений) это, кстати, довольно частый случай.
НЛО прилетело и опубликовало эту надпись здесь
Не осталось, если не считать следующие две:
1) Когда критично быстродействие и размер использованной памяти.
2) Когда массивы живут не только «внутри» чистого C++, где мы можем обмазываться контейнерами сколько угодно, но и активно куда-то передаются или откуда-то получаются. Я имею в виду связки C++ с высокоуровневыми ЯП, а также различные прикладные расширения C++ типа Cuda.
1) Когда критично быстродействие и размер использованной памяти.

И какой оверхед std::array привносит?

std::array ведь не умеет выводить размер из списка инициализации?..

НЛО прилетело и опубликовало эту надпись здесь
Как минимум, локальные «сырые» массивы выделяются на стеке, а не в хипе, что намного быстрее.

На этот случай есть std::array, впрочем, у него есть свои недостатки.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий