Вот фрагмент кода для вычисления квадратного корня из значений в массиве с плавающей точкой, взятых из
http://felix.abecassis.me/2011/09/cpp-getting-started-with-sse/
void sse(float* a, int N)
{
// We assume N % 4 == 0.
int nb_iters = N / 4;
__m128* ptr = (__m128*)a;for (int i = 0; i < nb_iters; ++i, ++ptr, a += 4){
_mm_store_ps(a, _mm_sqrt_ps(*ptr));
}
}
Когда я разбираю этот код, я вижу, что используется только один xmm (xmm0). Я предполагал, что развертывание цикла даст компилятору подсказку, что можно использовать больше xmms. Я изменил код как
void sse3(float* a, int N)
{
__m128* ptr = (__m128*)a;
for (int i = 0; i < N; i+=32){
_mm_store_ps(a + i, _mm_sqrt_ps(*ptr));
ptr++;
_mm_store_ps(a + i + 4, _mm_sqrt_ps(*ptr));
ptr++;
_mm_store_ps(a + i + 8, _mm_sqrt_ps(*ptr));
ptr++;
_mm_store_ps(a + i + 12, _mm_sqrt_ps(*ptr));
ptr++;
_mm_store_ps(a + i + 16, _mm_sqrt_ps(*ptr));
ptr++;
_mm_store_ps(a + i + 20, _mm_sqrt_ps(*ptr));
ptr++;
_mm_store_ps(a + i + 24, _mm_sqrt_ps(*ptr));
ptr++;
_mm_store_ps(a + i + 28, _mm_sqrt_ps(*ptr));
ptr++;
}
}
N должно быть больше 32 в этом случае.
Однако я до сих пор не вижу больше, чем один хмм. Почему компилятор не может назначить более одного xmm?
Насколько я понимаю, вычисления для xmm0, xmm1, xmm2 … xmm7 независимы и могут идти параллельно на современных суперскалярных архитектурах. На 4-ходовой суперскалярной машине второй фрагмент кода должен дать мне теоретическое ускорение 4 (например, если назначено 4 разных xmms).
PS: второй фрагмент кода, кажется, немного быстрее (последовательно).
sse3: 18809 microseconds
sse: 20543 microseconds
ОБНОВЛЕНО
Используется флаг -O3 как предложено
Вот разборка для ответа Бена Фойгта —
Обратите внимание, что я изменил название функции на sse4.
147:ssetest.cpp **** void sse4(float* a, int N)
148:ssetest.cpp **** {
2076 .loc 8 148 0
2077 .cfi_startproc
2078 .LVL173:
2079 .LBB5900:
2080 .LBB5901:
149:ssetest.cpp **** __m128 b, c, d, e;
150:ssetest.cpp ****
151:ssetest.cpp **** for (int i = 0; i < N; i += 16) {
2081 .loc 8 151 0
2082 0320 85F6 testl %esi, %esi # N
2083 0322 7E4C jle .L106 #,
147:ssetest.cpp **** void sse4(float* a, int N)
2084 .loc 8 147 0
2085 0324 8D56FF leal -1(%rsi), %edx #, tmp104
2086 .LBE5901:
2087 .LBE5900:
2088 0327 31C0 xorl %eax, %eax # ivtmp.1046
2089 .LBB5925:
2090 .LBB5924:
2091 0329 C1EA04 shrl $4, %edx #,
2092 032c 4883C201 addq $1, %rdx #, D.189746
2093 0330 48C1E206 salq $6, %rdx #, D.189746
2094 .LVL174:
2095 .p2align 4,,10
2096 0334 0F1F4000 .p2align 3
2097 .L108:
2098 .LBB5902:
2099 .LBB5903:
899:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h **** return (__m128) *(__v4sf *)__P;
2100 .loc 9 899 0 discriminator 2
2101 0338 0F285407 movaps 16(%rdi,%rax), %xmm2 # MEM[base: a_7(D), index: ivtmp.1046_85, offset: 16B], c
2101 10
2102 .LVL175:
2103 .LBE5903:
2104 .LBE5902:
2105 .LBB5904:
2106 .LBB5905:
182:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h **** return (__m128) __builtin_ia32_sqrtps ((__v4sf)__A);
2107 .loc 9 182 0 discriminator 2
2108 033d 0F511C07 sqrtps (%rdi,%rax), %xmm3 # MEM[base: a_7(D), index: ivtmp.1046_85, offset: 0B], tmp107
2109 .LBE5905:
2110 .LBE5904:
2111 .LBB5906:
2112 .LBB5907:
899:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h **** return (__m128) *(__v4sf *)__P;
2113 .loc 9 899 0 discriminator 2
2114 0341 0F284C07 movaps 32(%rdi,%rax), %xmm1 # MEM[base: a_7(D), index: ivtmp.1046_85, offset: 32B], d
2114 20
2115 .LVL176:
2116 .LBE5907:
2117 .LBE5906:
2118 .LBB5908:
2119 .LBB5909:
182:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h **** return (__m128) __builtin_ia32_sqrtps ((__v4sf)__A);
2120 .loc 9 182 0 discriminator 2
2121 0346 0F51D2 sqrtps %xmm2, %xmm2 # c, tmp109
2122 .LBE5909:
2123 .LBE5908:
2124 .LBB5910:
2125 .LBB5911:
899:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h **** return (__m128) *(__v4sf *)__P;
2126 .loc 9 899 0 discriminator 2
2127 0349 0F284407 movaps 48(%rdi,%rax), %xmm0 # MEM[base: a_7(D), index: ivtmp.1046_85, offset: 48B], e
2127 30
2128 .LVL177:
2129 .LBE5911:
2130 .LBE5910:
2131 .LBB5912:
2132 .LBB5913:
182:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h **** return (__m128) __builtin_ia32_sqrtps ((__v4sf)__A);
2133 .loc 9 182 0 discriminator 2
2134 034e 0F51C9 sqrtps %xmm1, %xmm1 # d, tmp111
2135 .LBE5913:
2136 .LBE5912:
2137 .LBB5914:
2138 .LBB5915:
2139 .loc 9 948 0 discriminator 2
2140 0351 0F291C07 movaps %xmm3, (%rdi,%rax) # tmp107, MEM[base: a_7(D), index: ivtmp.1046_85, offset: 0B]
2141 .LVL178:
2142 .LBE5915:
2143 .LBE5914:
2144 .LBB5916:
2145 .LBB5917:
182:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h **** return (__m128) __builtin_ia32_sqrtps ((__v4sf)__A);
2146 .loc 9 182 0 discriminator 2
2147 0355 0F51C0 sqrtps %xmm0, %xmm0 # e, tmp113
2148 .LBE5917:
2149 .LBE5916:
2150 .LBB5918:
2151 .LBB5919:
2152 .loc 9 948 0 discriminator 2
2153 0358 0F295407 movaps %xmm2, 16(%rdi,%rax) # tmp109, MEM[base: a_7(D), index: ivtmp.1046_85, offset: 16B]
2153 10
2154 .LVL179:
2155 .LBE5919:
2156 .LBE5918:
2157 .LBB5920:
2158 .LBB5921:
2159 035d 0F294C07 movaps %xmm1, 32(%rdi,%rax) # tmp111, MEM[base: a_7(D), index: ivtmp.1046_85, offset: 32B]
2159 20
2160 .LVL180:
2161 .LBE5921:
2162 .LBE5920:
2163 .LBB5922:
2164 .LBB5923:
2165 0362 0F294407 movaps %xmm0, 48(%rdi,%rax) # tmp113, MEM[base: a_7(D), index: ivtmp.1046_85, offset: 48B]
2165 30
2166 0367 4883C040 addq $64, %rax #, ivtmp.1046
2167 .LVL181:
2168 .LBE5923:
2169 .LBE5922:
2170 .loc 8 151 0 discriminator 2
2171 036b 4839D0 cmpq %rdx, %rax # D.189746, ivtmp.1046
2172 036e 75C8 jne .L108 #,
2173 .LVL182:
2174 .L106:
2175 0370 F3 rep
2176 0371 C3 ret
2177 .LBE5924:
2178 .LBE5925:
2179 .cfi_endproc
2180 .LFE7998:
2182 0372 66666666 .p2align 4,,15
2182 662E0F1F
2182 84000000
2182 0000
2183 .globl _Z6normalPfi
2185 _Z6normalPfi:
2186 .LFB7999:
152:ssetest.cpp **** b = _mm_load_ps(a + i);
153:ssetest.cpp **** c = _mm_load_ps(a + i + 4);
154:ssetest.cpp **** d = _mm_load_ps(a + i + 8);
155:ssetest.cpp **** e = _mm_load_ps(a + i + 12);
156:ssetest.cpp **** _mm_store_ps(a + i, _mm_sqrt_ps(b));
157:ssetest.cpp **** _mm_store_ps(a + i + 4, _mm_sqrt_ps(c));
158:ssetest.cpp **** _mm_store_ps(a + i + 8, _mm_sqrt_ps(d));
159:ssetest.cpp **** _mm_store_ps(a + i + 12, _mm_sqrt_ps(e));
160:ssetest.cpp **** }
161:ssetest.cpp **** }
Как ни странно, sse и sse4 имеют почти одинаковую производительность, а sse3 работает хуже всех (хотя часть цикла развернута).
Как насчет:
void sse3(float* a, int N)
{
__m128 b, c, d, e;
for (int i = 0; i < N; i += 16) {
b = _mm_load_ps(a + i);
c = _mm_load_ps(a + i + 4);
d = _mm_load_ps(a + i + 8);
e = _mm_load_ps(a + i + 12);
_mm_store_ps(a + i, _mm_sqrt_ps(b));
_mm_store_ps(a + i + 4, _mm_sqrt_ps(c));
_mm_store_ps(a + i + 8, _mm_sqrt_ps(d));
_mm_store_ps(a + i + 12, _mm_sqrt_ps(e));
}
}
NB. Использование одного и того же регистра XMM для нескольких вычислений — это то, что может убить конвейерную обработку, но это не единственное. Операции с различными регистрами могут выполняться независимо, только если другие ресурсы доступны в достаточном количестве. Нет целого блока вычисления SIMD, выделенного для каждого регистра, поскольку ваш комментарий предполагает, что вы верите.
Я думаю, что вы видите здесь компилятор, защищающий от псевдонимов. Вы читаете и пишете с помощью указателей, поэтому компилятор должен предполагать, что каждая запись в любой float *
может быть изменение следующего чтения через float *
, Попробуйте использовать __restrict__
в ваших указателях, чтобы сообщить компилятору, что вы этого не делаете. Вы также можете переписать свой цикл для вычисления из одного массива (глобальный идеален, потому что тогда псевдонимы не могут быть задействованы) в другой.