Какая разница между логическими SSE-характеристиками?

Есть ли разница между логическими SSE-функциями для разных типов? Например, если мы берем операцию ИЛИ, есть три свойства: _mm_or_ps, _mm_or_pd и _mm_or_si128, все из которых выполняют одно и то же: вычисляют побитовое ИЛИ их операндов. Мои вопросы:

  • Существует ли какая-либо разница между использованием одного или другого встроенного (с соответствующим типом). Не будет ли скрытых затрат, таких как более длительное выполнение в какой-то конкретной ситуации?

  • Эти внутренности отображаются в трех разных командах x86 (por, orps, orpd). У кого-нибудь есть идеи, почему Intel тратит драгоценное пространство opcode на несколько инструкций, которые делают то же самое?

Ответы

Ответ 1

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

#include <stdio.h>
#include <emmintrin.h>
#include <pmmintrin.h>
#include <xmmintrin.h>

int main(void)
{
    __m128i a = _mm_set1_epi32(1);
    __m128i b = _mm_set1_epi32(2);
    __m128i c = _mm_or_si128(a, b);

    __m128 x = _mm_set1_ps(1.25f);
    __m128 y = _mm_set1_ps(1.5f);
    __m128 z = _mm_or_ps(x, y);

    printf("a = %vld, b = %vld, c = %vld\n", a, b, c);
    printf("x = %vf, y = %vf, z = %vf\n", x, y, z);

    c = (__m128i)_mm_or_ps((__m128)a, (__m128)b);
    z = (__m128)_mm_or_si128((__m128i)x, (__m128i)y);

    printf("a = %vld, b = %vld, c = %vld\n", a, b, c);
    printf("x = %vf, y = %vf, z = %vf\n", x, y, z);

    return 0;
}

$ gcc -Wall -msse3 por.c -o por

$ ./por

a = 1 1 1 1, b = 2 2 2 2, c = 3 3 3 3
x = 1.250000 1.250000 1.250000 1.250000, y = 1.500000 1.500000 1.500000 1.500000, z = 1.750000 1.750000 1.750000 1.750000
a = 1 1 1 1, b = 2 2 2 2, c = 3 3 3 3
x = 1.250000 1.250000 1.250000 1.250000, y = 1.500000 1.500000 1.500000 1.500000, z = 1.750000 1.750000 1.750000 1.750000

Ответ 2

В соответствии с рекомендациями по оптимизации Intel и AMD смешивание типов операций с типами данных приводит к поражению производительности, поскольку ЦП внутренне теряет 64-битные половинки регистра для определенного типа данных. Это, по-видимому, в основном влияет на облицовку труб, когда команда декодируется, и запланированы команды. Функционально они дают одинаковый результат. Более новые версии для целых типов данных имеют большую кодировку и занимают больше места в сегменте кода. Поэтому, если размер кода является проблемой, используйте старые операционные системы, поскольку они имеют меньшую кодировку.

Ответ 3

  • Есть ли разница между использованием того или иного внутреннего (с соответствующим типом). Не будут ли скрытые затраты, такие как длительное исполнение в какой-то конкретной ситуации?

Да, могут быть причины производительности, чтобы выбрать один против другого.

1: Иногда возникает дополнительный цикл или два задержки (пересылка), если вывод целочисленного исполняющего устройства должен быть перенаправлен на вход блока исполнения FP или наоборот, Требуется много проводов для перемещения 128b данных в любое из многих возможных мест назначения, поэтому разработчикам процессоров приходится делать компромиссы, например, только имея прямой путь от каждого выхода FP до каждого входа FP, а не ВСЕ возможные входы.

См. этот ответ или документ микроархитектуры Agner Fog для обхода-задержки, Найдите "Задержки на передачу данных в Nehalem" в документе Agner doc; у него есть несколько хороших практических примеров и дискуссий. У него есть раздел на нем для каждого микроархата, который он проанализировал.

Однако задержки передачи данных между разные домены или разные типы регистров на Sandy Bridge и Ivy Bridge, чем на Nehalem, и часто ноль. - Agner Fog micro arch doc

Помните, что время ожидания не имеет значения, если оно не находится на критическом пути вашего кода. Использование pshufd вместо movaps + shufps может быть выигрышем, если пропускная способность uop - ваше узкое место, а не латентность вашего критического пути.

2: Версия ...ps занимает 1 байт кода, чем два других. Это приведет к выравниванию следующих инструкций по-разному, что может иметь значение для строк декодера и/или uop cache.

3: Последние процессоры Intel могут запускать только версии FP на порту5.

  • Merom (Core2) и Penryn: orps могут выполняться на p0/p1/p5, но только в целых доменах. Предположительно, все 3 версии декодируются в тот же самый uop. Таким образом происходит задержка пересылки между доменами. (Процессоры AMD делают это тоже: побитовые инструкции FP запускаются в домене ivec.)

  • Nehalem/Sandybridge/IvB/Haswell/Broadwell: por может работать на p0/p1/p5, но orps может работать только на port5. p5 также необходим для перетасовки, но модули FMA, FP add и FP mul находятся на портах 0/1.

  • Skylake: por и orps имеют пропускную способность 3 на каждый цикл. Информация о задержках пересылки еще не доступна.

Обратите внимание, что на SnB/IvB (AVX, но не на AVX2) только p5 должен обрабатывать 256 бит логических операций, так как vpor ymm, ymm требует AVX2. Вероятно, это было не повод для изменения, так как Nehalem сделал это.

Как правильно выбирать:

Если логическая операционная пропускная способность на порту5 может быть узким местом, используйте целые версии, даже для данных FP. Это особенно актуально, если вы хотите использовать целые перетасовки или другие инструкции по перемещению данных.

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

Если вы просто хотите установить/очистить/перевернуть бит в FP-векторах между инструкциями FP add и mul, используйте логики ...ps, даже для данных с двойной точностью, поскольку одиночные и двойные FP являются одним и тем же доменом на каждом CPU существует, а версии ...ps на один байт короче.

Существуют практические/человеческие факторы для использования версий ...pd, хотя они часто перевешивают сохранение 1 байта кода. Читаемость вашего кода другими людьми является фактором: они будут задаваться вопросом, почему вы обрабатываете свои данные как синглы, когда они фактически удваиваются. Особенно с внутренними свойствами C/С++, засоряя ваш код приложением между __mm256 и __mm256d, не стоит. Если настройка на уровне insn-выравнивания имеет значение, напишите в asm напрямую, а не intrinsics! (Имея инструкцию длиной более одного байта, вы могли бы лучше подобрать для плотности и/или декодера кеш-памяти кеш-памяти).

Для целочисленных данных используйте целые версии. Сохранение одного байта команды не стоит задержка байпаса, а целочисленный код часто удерживает port5 полностью занятым тасованием. Для Haswell многие команды shuffle/insert/extract/pack/unpack стали только p5, вместо p1/p5 для SnB/IvB.

  1. Эти встроенные функции сопоставляются с тремя различными инструкциями x86 (por, orps, orpd). У кого-нибудь есть идеи, почему Intel тратит драгоценный опкод пространство для нескольких инструкций, которые делают то же самое?

Если вы посмотрите на историю этих наборов инструкций, вы можете увидеть, как мы сюда попали.

por  (MMX):     0F EB /r
orps (SSE):     0F 56 /r
orpd (SSE2): 66 0F 56 /r
por  (SSE2): 66 0F EB /r

MMX существовал до SSE, поэтому он выглядит как коды операций для SSE (...ps) инструкций были выбраны из того же пространства 0F xx. Затем для SSE2 версия ...pd добавила префикс размера 66 операнда к коду ...ps, а целочисленная версия добавила префикс 66 к версии MMX.

Они могли бы оставить orpd и/или por, но они этого не сделали. Возможно, они думали, что будущие конструкции процессоров могут иметь более длинные пути пересылки между разными доменами, и поэтому использование соответствующей команды для ваших данных будет более выгодным. Несмотря на то, что существуют отдельные коды операций, AMD и ранняя Intel относились ко всем тем же, что и int-vector.