Почему этот код SSE2 работает непоследовательно?

В качестве учебного упражнения я пробую свои силы в ускорении кода умножения матриц с использованием SIMD на различных архитектурах. У меня странная проблема с моим кодом умножения 3D-матриц для SSE2, где его производительность колеблется между двумя крайностями, либо ~ 5 мс (ожидается), либо ~ 100 мс для 1 миллиона операций.

Единственное, что "плохо", что делает этот код, это невыровненные сохранения/загрузки и хак в конце для сохранения вектора в памяти без 4-го элемента, вытаптывающего память. Это могло бы объяснить некоторую разницу в производительности, но тот факт, что разница в производительности настолько велика, заставляет меня подозревать, что я упускаю что-то важное.

Я пробовал пару вещей, но я попробую еще раз после сна.

См. код ниже. Переменная m_matrix выравнивается по границе 16 байт.

void Matrix3x3::MultiplySSE2(Matrix3x3 &other, Matrix3x3 &output)
{
    __m128 a_row, r_row;
    __m128 a1_row, r1_row;
    __m128 a2_row, r2_row;

    const __m128 b_row0 = _mm_load_ps(&other.m_matrix[0]);
    const __m128 b_row1 = _mm_loadu_ps(&other.m_matrix[3]);
    const __m128 b_row2 = _mm_loadu_ps(&other.m_matrix[6]);

    // Perform dot products with first row
    a_row = _mm_set1_ps(m_matrix[0]);
    r_row = _mm_mul_ps(a_row, b_row0);
    a_row = _mm_set1_ps(m_matrix[1]);
    r_row = _mm_add_ps(_mm_mul_ps(a_row, b_row1), r_row);
    a_row = _mm_set1_ps(m_matrix[2]);
    r_row = _mm_add_ps(_mm_mul_ps(a_row, b_row2), r_row);

    _mm_store_ps(&output.m_matrix[0], r_row);

    // Perform dot products with second row
    a1_row = _mm_set1_ps(m_matrix[3]);
    r1_row = _mm_mul_ps(a1_row, b_row0);
    a1_row = _mm_set1_ps(m_matrix[4]);
    r1_row = _mm_add_ps(_mm_mul_ps(a1_row, b_row1), r1_row);
    a1_row = _mm_set1_ps(m_matrix[5]);
    r1_row = _mm_add_ps(_mm_mul_ps(a1_row, b_row2), r1_row);

    _mm_storeu_ps(&output.m_matrix[3], r1_row);

    // Perform dot products with third row
    a2_row = _mm_set1_ps(m_matrix[6]);
    r2_row = _mm_mul_ps(a2_row, b_row0);
    a2_row = _mm_set1_ps(m_matrix[7]);
    r2_row = _mm_add_ps(_mm_mul_ps(a2_row, b_row1), r2_row);
    a2_row = _mm_set1_ps(m_matrix[8]);
    r2_row = _mm_add_ps(_mm_mul_ps(a2_row, b_row2), r2_row);

    // Store only the first 3 elements in a vector so we dont trample memory
    _mm_store_ss(&output.m_matrix[6], _mm_shuffle_ps(r2_row, r2_row,        _MM_SHUFFLE(0, 0, 0, 0)));
    _mm_store_ss(&output.m_matrix[7], _mm_shuffle_ps(r2_row, r2_row, _MM_SHUFFLE(1, 1, 1, 1)));
    _mm_store_ss(&output.m_matrix[8], _mm_shuffle_ps(r2_row, r2_row, _MM_SHUFFLE(2, 2, 2, 2)));
}

person BlamKiwi    schedule 19.07.2015    source источник
comment
Сделайте 1 миллион тестов несколько раз и получите среднее время. Выполнение теста только один раз даст вам ненадежные результаты.   -  person RamblingMad    schedule 19.07.2015
comment
@CoffeeandCode Я беру среднее значение уже как часть испытательного стенда. Среднее время по-прежнему является аномалией по сравнению с 2D- и 4D-матрицами с SSE2. Пошаговое поведение нельзя объяснить статистикой, код неисправен.   -  person BlamKiwi    schedule 19.07.2015
comment
Идк, чувак. Если он работает, я бы не сказал, что он неисправен. Кстати, почему вы делаете невыровненный магазин?   -  person RamblingMad    schedule 19.07.2015
comment
@CoffeeandCode Строки матрицы 3x3 имеют шаг 12 байтов, но выровненные инструкции в SSE требуют, чтобы адреса были выровнены по границе 16 байтов. Таким образом, после первой строки мы не можем использовать выровненную загрузку/сохранение.   -  person BlamKiwi    schedule 19.07.2015
comment
Вот почему я рекомендую использовать только матрицы 4x4 или 3x4, если вы действительно не ограничены в пространстве. В другой области: вы запускаете эти тесты с другими потоками, касающимися одних и тех же данных? Конечно, это открывает ряд других вопросов, например, какая архитектура и т. д. Чтобы сузить круг вопросов, попробуйте вариант только с выравниванием, который работает с данными подходящего размера.   -  person defube    schedule 19.07.2015
comment
@defube, но идти по легкому пути неинтересно. ;) Я планирую попробовать использовать подход только с выравниванием, который использует больше перетасовок и меньше загрузок/хранений. Я должен буду видеть, как это идет.   -  person BlamKiwi    schedule 20.07.2015
comment
Вообще 3х3 сложно сделать хорошо. Для библиотеки DirectXMath умножение матрицы всегда 4x4, но есть функции для загрузки/сохранения матрицы 3x3 (и 3x4) в структуре данных. На самом деле, вы должны взглянуть на DirectXMath, так как вся реализация выполняется во встроенных функциях и полностью встроена.   -  person Chuck Walbourn    schedule 20.07.2015
comment
Тот факт, что вы интенсивно используете mm_set1_ps, невыровненные загрузки и скалярные хранилища, означает, что реализации не хватает потенциальной производительности. Если m_matrix всегда гарантированно будет выровнено по 16 байтам, то матрица 4x4 будет занимать столько же места, сколько и все это заполнение, и она гораздо лучше подходит для SSE/SSE2.   -  person Chuck Walbourn    schedule 20.07.2015
comment
Еще одна вещь, которую следует помнить о SIMD: выполнение операции с 9 числами с плавающей запятой из памяти, а затем запись их обратно в память не принесет особой пользы и, конечно, не даст ничего близкого к теоретическому 4-кратному ускорению. Вам нужно загрузить данные из памяти, выполнить с ними множество SIMD-дружественных вычислений, а затем записать результаты. В противном случае любое ускорение, которое вы получите от нескольких умножений и сложений SIMD, в любом случае теряется из-за перегрузки загрузки и хранения данных. Вот почему все DirectXMath является встроенным, а функции потребляют/возвращают XMVECTOR/XMMATRIX, чтобы компилятор объединил их.   -  person Chuck Walbourn    schedule 20.07.2015
comment
@ChuckWalbourn Я определенно понимаю, почему производственная реализация будет использовать матрицы 3x4 и тратить столько усилий, заставляя компилятор объединять регистры. Это было больше упражнение в том, что/почему. После трех реализаций чистого кода 3x3 код 3x4 оказался быстрее и не страдал от странного снижения производительности. Так что, пожалуй, я просто брошу это.   -  person BlamKiwi    schedule 20.07.2015


Ответы (1)


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

проблемы с производительностью в вашем коде (которые не объясняют дисперсию в 20 раз. Они всегда должны быть медленными):

_mm_set1_ps(m_matrix[3]) и так далее будет проблемой. Для трансляции элемента требуется pshufd или movaps + shufps. Я думаю, что это неизбежно для матмуль, хотя.

Сохранение последних 3 элементов без записи после конца: попробуйте PALIGNR получить последний элемент предыдущей строки в reg с последней строкой. Затем вы можете сделать одно невыровненное хранилище, которое перекрывается с предыдущим хранилищем. Это намного меньше перетасовок и, вероятно, быстрее, чем movss / extractps / extractps.

Если вы хотите попробовать что-то с меньшим количеством невыровненных 16-байтовых хранилищ, попробуйте movss, перемешайте или сдвиньте вправо на 4 байта (psrldq или _mm_bsrli_si128), затем movq или movsd, чтобы сохранить последние 8 байтов за один раз. (побайтовый сдвиг находится на том же порту выполнения, что и перемешивание, в отличие от битовых сдвигов для каждого элемента)

Почему вы сделали три _mm_shuffle_ps (shufps)? Нижний элемент уже тот, который вам нужен, для первого столбца последней строки. Во всяком случае, я думаю, что extractps быстрее, чем перемешивание + сохранение, на не-AVX, где сохранение источника от затирания shufps требует движения. pshufd подойдет.)

person Peter Cordes    schedule 20.07.2015
comment
Еще одна вещь, о которой следует помнить, это то, что это действительно зависит от точности, какие наборы инструкций вы можете использовать. Использование только SSE/SSE2 ограничивает, но обеспечивает широкую совместимость — например, это требуется для x64, поэтому все процессоры с поддержкой x64 должны его поддерживать. Однако существует множество полезные наборы инструкций помимо SSE2. - person Chuck Walbourn; 20.07.2015
comment
Да, версия shufps с 3 операндами AVX позволит более эффективно транслировать отдельные элементы. Вы можете выполнить 2 загрузки по 16 байт и 8 байт для левой стороны, поэтому m_matrix[] находится в 3 регистрах. Хотя с AVX VBROADCASTSS из памяти - это всего лишь один uop, которому даже вообще не нужны порты в случайном порядке. _mm_set1_ps компилируется с AVX на gcc. FMA также уменьшит количество мопов. - person Peter Cordes; 20.07.2015
comment
@PeterCordes Я буду помнить об этом, но я собираюсь отказаться от этой реализации и попробовать другой подход. - person BlamKiwi; 20.07.2015
comment
Наверное, хороший звонок. В последнее время здесь было много вопросов о коротких векторах/матрицах, некоторые с интересными ответами. Я забыл детали, потому что это не имело прямого отношения ко мне прямо сейчас! - person Peter Cordes; 20.07.2015
comment
Имма все равно отметит это как ответ, у него был хороший совет по работе с реализацией (хотя он и ошибочен) - person BlamKiwi; 21.07.2015