2014-09-16 2 views
2

У меня есть общий код, который я пытаюсь переместить в SSE, чтобы ускорить его, так как его часто называют. Код в вопросе в основном что-то вроде этого:Неизвестное узкое место SSE

for (int i = 1; i < mysize; ++i) 
{ 
    buf[i] = myMin(buf[i], buf[i - 1] + offset); 
} 

где myMin ваша простая функция мин (а < б)? а: Ь (я смотрел на разборку и есть скачки здесь)

Мой SSE код (который я прошел через несколько итераций для ускорения) находится в этой форме в настоящее время:

float tmpf = *(tmp - 1); 
__m128 off = _mm_set_ss(offset); 
for (int l = 0; l < mysize; l += 4) 
{ 
    __m128 post = _mm_load_ps(tmp); 
    __m128 pre = _mm_move_ss(post, _mm_set_ss(tmpf)); 
    pre = _mm_shuffle_ps(pre, pre, _MM_SHUFFLE(0, 3, 2, 1)); 
    pre = _mm_add_ss(pre, off); 
    post = _mm_min_ss(post, pre); 

    // reversed 
    pre = _mm_shuffle_ps(post, post, _MM_SHUFFLE(2, 1, 0, 3)); 
    post = _mm_add_ss(post, off); 
    pre = _mm_min_ss(pre, post); 

    post = _mm_shuffle_ps(pre, pre, _MM_SHUFFLE(2, 1, 0, 3)); 
    pre = _mm_add_ss(pre, off); 
    post = _mm_min_ss(post, pre); 

    // reversed 
    pre = _mm_shuffle_ps(post, post, _MM_SHUFFLE(2, 1, 0, 3)); 
    post = _mm_add_ss(post, off); 
    pre = _mm_min_ss(pre, post); 

    post = _mm_shuffle_ps(pre, pre, _MM_SHUFFLE(2, 1, 0, 3)); 
    _mm_store_ps(tmp, post); 
    tmpf = tmp[3]; 
    tmp += 4; 
} 

Игнорирование любых сценариев кэш-памяти, с которыми я справился, и накладные расходы для них незначительны из-за размера buf/tmp, может ли кто-нибудь объяснить, почему версия SSE медленнее на 2x? VTune отнесет его к промахам L1, но, как я вижу, он должен сделать 4 раза меньше поездок на L1 и никаких ветвей/прыжков, поэтому должен быть быстрее, но это не так. Что я тут ошибаюсь?

Благодаря

EDIT: Так что я найти что-нибудь в отдельном тесте. Я не думал, что это будет иметь значение, но, увы, это так. Так что mysize above на самом деле не такой большой (около 30-50), но их много, и все они выполняются серийно. В этом случае тройное выражение выполняется быстрее, чем SSE. Однако, если он изменен на mysize, находясь в миллионах, и есть только 30-50 итераций из них, версия SSE выполняется быстрее. Любая идея почему? Я думаю, что взаимодействие памяти будет одинаковым для обоих, в том числе упреждающего упреждающей и т.д ...

+1

Является ли версия SSE более параллельной, чем оригинал? – user2357112

+3

Последовательная зависимость - вот что убивает вас здесь - это делает цикл непригодным для векторизации SIMD, и в результате вы делаете большую работу в цикле SIMD. Вероятнее всего, было бы более полезно сосредоточиться на оптимизации скалярного цикла: убедитесь, что вы используете нераспределенный минимум без ненужного плавающего <-> двойных преобразований и, возможно, также разворачиваете цикл вручную (будьте осторожны с зависимостями, конечно). –

+1

Профилировщик ошибочен. Этот цикл не является векторизуемым - если вы не попробуете что-то вроде параллельного префикса min. – Mysticial

ответ

0

Вот внеофисный решение, которое вы могли бы попробовать

for (int i = 1; i < mysize; i++) { 
    float a = buf[i]; 
    float b = buf[i-1] + offset; 
    buf[i] = b + (a<b)*(a-b); 
} 

Вот сборка:

.L6: 
addss xmm0, xmm4 
movss xmm1, DWORD PTR [rax] 
movaps xmm2, xmm1 
add rax, 4 
movaps xmm3, xmm6 
cmpltss xmm2, xmm0 
subss xmm1, xmm0 
andps xmm3, xmm2 
andnps xmm2, xmm5 
orps xmm2, xmm3 
mulss xmm1, xmm2 
addss xmm0, xmm1 
movss DWORD PTR [rax-4], xmm0 
cmp rax, rdx 
jne .L6 

Но версия с ответвлением, вероятно, уже лучше

for (int i = 1; i < mysize; i++) { 
    float a = buf[i]; 
    float b = buf[i-1] + offset; 
    buf[i] = a<b ? a : b; 
} 

Здесь я евы Ассамблеи

.L15: 
addss xmm0, xmm2 
movss xmm1, DWORD PTR [rax] 
add rax, 4 
minss xmm1, xmm0 
movss DWORD PTR [rax-4], xmm1 
cmp rax, rdx 
movaps xmm0, xmm1 
jne .L15 

Это создает код, который в любом случае внеофисное использование minss (cmp rax, rdx относится к итератору цикла).

Наконец, вот код, который вы можете использовать с MSVC, который производит один и тот же узел, как GCC, который внеофисный

__m128 offset4 = _mm_set1_ps(offset); 
for (int i = 1; i < mysize; i++) { 
    __m128 a = _mm_load_ss(&buf[i]); 
    __m128 b = _mm_load_ss(&buf[i-1]); 
    b = _mm_add_ss(b, offset4); 
    a = _mm_min_ss(a,b); 
    _mm_store_ss(&buf[i], a); 
} 

Вот другая форма, вы можете попробовать, который использует ветвь

__m128 offset4 = _mm_set1_ps(offset); 
for (int i = 1; i < mysize; i++) { 
    __m128 a = _mm_load_ss(&buf[i]); 
    __m128 b = _mm_load_ss(&buf[i-1]); 
    b = _mm_add_ss(b, offset4); 
    if(_mm_comige_ss(b,a)) 
     _mm_store_ss(&buf[i], b); 
} 
+0

Несмотря на то, что он не имеет разветвлений, он использует больше инструкций, чем требуется. В идеале для x86 вы хотели бы использовать «FCMOV». –

+0

@PaulR, я вижу вашу точку зрения. Я обновил свой ответ. Версия с веткой, вероятно, уже достаточно хороша (и лучше). На самом деле у него нет ветки, так как она использует 'minss'. Если я скомпилирую в 32-битном режиме, я получаю 'fcmov', но я не думаю, что это хорошая идея.Вероятно, лучше всего использовать 'minss'. –

+0

Я думаю, что основная проблема заключается в том, что OP использует Visual Studio, и это генерирует разветвленный код. gcc и др. уже достаточно умны, чтобы генерировать бесконтактный код без какой-либо дополнительной помощи. –

1

Если этот код критичен по производительности, вам придется посмотреть на данные, которые вы получаете. Это серийная зависимость, которая убивает вас, и вам нужно избавиться от нее.

Одно очень маленькое значение buf [i] будет влиять на многие из следующих значений. Например, если offset = 1, buf [0] = 0, а все остальные значения -> 1 миллион, то одно значение будет влиять на следующий миллион. С другой стороны, такого рода вещи могут случаться очень редко.

Если это редкость, то вы полностью проверяете, есть ли buf [i]> buf [i] + offset, замените его, если это так, и отслеживайте, где были сделаны изменения, не учитывая, что значения buf [i] могут стекать вверх. Затем вы проверяете, где были сделаны изменения, и перепроверьте их.

В крайних случаях, скажем, buf [i] всегда находится между 0 и 1 и смещением> 0,5, вы знаете, что buf [i] вообще не может влиять на buf [i + 2], поэтому вы просто игнорируете последовательную зависимость и делать все параллельно, полностью векторизованными.

С другой стороны, если у вас есть несколько небольших значений в вашем буфере, которые влияют на большое количество последовательных значений, то вы начинаете с первого значения buf [0] и полностью векторизовали, проверяете ли buf [i] < buf [0 ] + i * offset, заменяя значения, пока проверка не завершится.

Вы говорите, что «значения могут быть любыми». Если это так, например, если buf [i] выбирается случайным образом где-то между 0 и 1 000 000, а смещение не очень велико, тогда у вас будут элементы buf [i], которые заставляют множество следующих элементов быть buf [i] + (k - i) * смещение. Например, если offset = 1, и вы обнаружите, что buf [i] составляет около 10000, тогда он заставит в среднем около 100 значений равняться смещению buf [i] + (k - i) *.

+1

Ваш ответ можно суммировать с моим комментарием «Если бы вы знали, что buf [i] было больше, чем buf [i-1] в большинстве случаев, тогда вы, вероятно, могли бы использовать _mm_movemask_epi8, чтобы ускорить это», а OP ответил «Значения может быть что угодно ». который я подразумеваю, означает, что он не может ничего рассказать о статистике распределения. –

+0

Идея состоит в том, чтобы полностью игнорировать зависимости, поэтому вы получаете полностью векторный код, а затем фиксируете бит, где вы поступили неправильно. Невозможность предположить что-либо о распределении значений - это копать. Текущий код в основном скалярный _and_ имеет зависимости. Vectorising делает его, вероятно, в 10 раз быстрее, и это требует поиска данных. – gnasher729

+0

Затем введите код, показывающий, как это сделать. Я сомневаюсь, что это поможет случайным данным. На самом деле я думаю, что было бы хуже предположить, что никакая зависимость не будет исправлена ​​для случайных данных. Я думаю, что интеллектуальный метод мог отслеживать статистику по мере продвижения, и если бы он обнаружил, что данные были не очень случайными, можно предположить, что данные не были случайными. В этом случае я мог видеть выгоду, но это работа. –

Смежные вопросы