Я пытаюсь убедиться, что gcc векторизует мои циклы. Оказывается, с помощью -march=znver1
(или -march=native
) gcc пропускает некоторые циклы, хотя их можно векторизовать. Почему это происходит?
В этом коде второй цикл, который умножает каждый элемент на скаляр, не векторизован:
#include <stdio.h>
#include <inttypes.h>
int main() {
const size_t N = 1000;
uint64_t arr[N];
for (size_t i = 0; i < N; ++i)
arr[i] = 1;
for (size_t i = 0; i < N; ++i)
arr[i] *= 5;
for (size_t i = 0; i < N; ++i)
printf("%lu\n", arr[i]); // use the array so that it is not optimized away
}
gcc -O3 -fopt-info-vec-all -mavx2 main.c
:
main.cpp:13:26: missed: couldn't vectorize loop
main.cpp:14:15: missed: statement clobbers memory: printf ("%lu\n", _3);
main.cpp:10:26: optimized: loop vectorized using 32 byte vectors
main.cpp:7:26: optimized: loop vectorized using 32 byte vectors
main.cpp:4:5: note: vectorized 2 loops in function.
main.cpp:14:15: missed: statement clobbers memory: printf ("%lu\n", _3);
main.cpp:15:1: note: ***** Analysis failed with vector mode V4DI
main.cpp:15:1: note: ***** Skipping vector mode V32QI, which would repeat the analysis for V4DI
gcc -O3 -fopt-info-vec-all -march=znver1 main.c
:
main.cpp:13:26: missed: couldn't vectorize loop
main.cpp:14:15: missed: statement clobbers memory: printf ("%lu\n", _3);
main.cpp:10:26: missed: couldn't vectorize loop
main.cpp:10:26: missed: not vectorized: unsupported data-type
main.cpp:7:26: optimized: loop vectorized using 16 byte vectors
main.cpp:4:5: note: vectorized 1 loops in function.
main.cpp:14:15: missed: statement clobbers memory: printf ("%lu\n", _3);
main.cpp:15:1: note: ***** Analysis failed with vector mode V2DI
main.cpp:15:1: note: ***** Skipping vector mode V16QI, which would repeat the analysis for V2DI
-march=znver1
включает -mavx2
, поэтому я думаю, что gcc по какой-то причине предпочитает не векторизовать его:
~ $ gcc -march=znver1 -Q --help=target
The following options are target specific:
-m128bit-long-double [enabled]
-m16 [disabled]
-m32 [disabled]
-m3dnow [disabled]
-m3dnowa [disabled]
-m64 [enabled]
-m80387 [enabled]
-m8bit-idiv [disabled]
-m96bit-long-double [disabled]
-mabi= sysv
-mabm [enabled]
-maccumulate-outgoing-args [disabled]
-maddress-mode= long
-madx [enabled]
-maes [enabled]
-malign-data= compat
-malign-double [disabled]
-malign-functions= 0
-malign-jumps= 0
-malign-loops= 0
-malign-stringops [enabled]
-mamx-bf16 [disabled]
-mamx-int8 [disabled]
-mamx-tile [disabled]
-mandroid [disabled]
-march= znver1
-masm= att
-mavx [enabled]
-mavx2 [enabled]
-mavx256-split-unaligned-load [disabled]
-mavx256-split-unaligned-store [enabled]
-mavx5124fmaps [disabled]
-mavx5124vnniw [disabled]
-mavx512bf16 [disabled]
-mavx512bitalg [disabled]
-mavx512bw [disabled]
-mavx512cd [disabled]
-mavx512dq [disabled]
-mavx512er [disabled]
-mavx512f [disabled]
-mavx512ifma [disabled]
-mavx512pf [disabled]
-mavx512vbmi [disabled]
-mavx512vbmi2 [disabled]
-mavx512vl [disabled]
-mavx512vnni [disabled]
-mavx512vp2intersect [disabled]
-mavx512vpopcntdq [disabled]
-mavxvnni [disabled]
-mbionic [disabled]
-mbmi [enabled]
-mbmi2 [enabled]
-mbranch-cost=<0,5> 3
-mcall-ms2sysv-xlogues [disabled]
-mcet-switch [disabled]
-mcld [disabled]
-mcldemote [disabled]
-mclflushopt [enabled]
-mclwb [disabled]
-mclzero [enabled]
-mcmodel= [default]
-mcpu=
-mcrc32 [disabled]
-mcx16 [enabled]
-mdispatch-scheduler [disabled]
-mdump-tune-features [disabled]
-menqcmd [disabled]
-mf16c [enabled]
-mfancy-math-387 [enabled]
-mfentry [disabled]
-mfentry-name=
-mfentry-section=
-mfma [enabled]
-mfma4 [disabled]
-mforce-drap [disabled]
-mforce-indirect-call [disabled]
-mfp-ret-in-387 [enabled]
-mfpmath= sse
-mfsgsbase [enabled]
-mfunction-return= keep
-mfused-madd -ffp-contract=fast
-mfxsr [enabled]
-mgeneral-regs-only [disabled]
-mgfni [disabled]
-mglibc [enabled]
-mhard-float [enabled]
-mhle [disabled]
-mhreset [disabled]
-miamcu [disabled]
-mieee-fp [enabled]
-mincoming-stack-boundary= 0
-mindirect-branch-register [disabled]
-mindirect-branch= keep
-minline-all-stringops [disabled]
-minline-stringops-dynamically [disabled]
-minstrument-return= none
-mintel-syntax -masm=intel
-mkl [disabled]
-mlarge-data-threshold=<number> 65536
-mlong-double-128 [disabled]
-mlong-double-64 [disabled]
-mlong-double-80 [enabled]
-mlwp [disabled]
-mlzcnt [enabled]
-mmanual-endbr [disabled]
-mmemcpy-strategy=
-mmemset-strategy=
-mmitigate-rop [disabled]
-mmmx [enabled]
-mmovbe [enabled]
-mmovdir64b [disabled]
-mmovdiri [disabled]
-mmpx [disabled]
-mms-bitfields [disabled]
-mmusl [disabled]
-mmwaitx [enabled]
-mneeded [disabled]
-mno-align-stringops [disabled]
-mno-default [disabled]
-mno-fancy-math-387 [disabled]
-mno-push-args [disabled]
-mno-red-zone [disabled]
-mno-sse4 [disabled]
-mnop-mcount [disabled]
-momit-leaf-frame-pointer [disabled]
-mpc32 [disabled]
-mpc64 [disabled]
-mpc80 [disabled]
-mpclmul [enabled]
-mpcommit [disabled]
-mpconfig [disabled]
-mpku [disabled]
-mpopcnt [enabled]
-mprefer-avx128 -mprefer-vector-width=128
-mprefer-vector-width= 128
-mpreferred-stack-boundary= 0
-mprefetchwt1 [disabled]
-mprfchw [enabled]
-mptwrite [disabled]
-mpush-args [enabled]
-mrdpid [disabled]
-mrdrnd [enabled]
-mrdseed [enabled]
-mrecip [disabled]
-mrecip=
-mrecord-mcount [disabled]
-mrecord-return [disabled]
-mred-zone [enabled]
-mregparm= 6
-mrtd [disabled]
-mrtm [disabled]
-msahf [enabled]
-mserialize [disabled]
-msgx [disabled]
-msha [enabled]
-mshstk [disabled]
-mskip-rax-setup [disabled]
-msoft-float [disabled]
-msse [enabled]
-msse2 [enabled]
-msse2avx [disabled]
-msse3 [enabled]
-msse4 [enabled]
-msse4.1 [enabled]
-msse4.2 [enabled]
-msse4a [enabled]
-msse5 -mavx
-msseregparm [disabled]
-mssse3 [enabled]
-mstack-arg-probe [disabled]
-mstack-protector-guard-offset=
-mstack-protector-guard-reg=
-mstack-protector-guard-symbol=
-mstack-protector-guard= tls
-mstackrealign [disabled]
-mstringop-strategy= [default]
-mstv [enabled]
-mtbm [disabled]
-mtls-dialect= gnu
-mtls-direct-seg-refs [enabled]
-mtsxldtrk [disabled]
-mtune-ctrl=
-mtune= znver1
-muclibc [disabled]
-muintr [disabled]
-mvaes [disabled]
-mveclibabi= [default]
-mvect8-ret-in-mem [disabled]
-mvpclmulqdq [disabled]
-mvzeroupper [enabled]
-mwaitpkg [disabled]
-mwbnoinvd [disabled]
-mwidekl [disabled]
-mx32 [disabled]
-mxop [disabled]
-mxsave [enabled]
-mxsavec [enabled]
-mxsaveopt [enabled]
-mxsaves [enabled]
Known assembler dialects (for use with the -masm= option):
att intel
Known ABIs (for use with the -mabi= option):
ms sysv
Known code models (for use with the -mcmodel= option):
32 kernel large medium small
Valid arguments to -mfpmath=:
387 387+sse 387,sse both sse sse+387 sse,387
Known indirect branch choices (for use with the -mindirect-branch=/-mfunction-return= options):
keep thunk thunk-extern thunk-inline
Known choices for return instrumentation with -minstrument-return=:
call none nop5
Known data alignment choices (for use with the -malign-data= option):
abi cacheline compat
Known vectorization library ABIs (for use with the -mveclibabi= option):
acml svml
Known address mode (for use with the -maddress-mode= option):
long short
Known preferred register vector length (to use with the -mprefer-vector-width= option):
128 256 512 none
Known stack protector guard (for use with the -mstack-protector-guard= option):
global tls
Valid arguments to -mstringop-strategy=:
byte_loop libcall loop rep_4byte rep_8byte rep_byte unrolled_loop vector_loop
Known TLS dialects (for use with the -mtls-dialect= option):
gnu gnu2
Known valid arguments for -march= option:
i386 i486 i586 pentium lakemont pentium-mmx winchip-c6 winchip2 c3 samuel-2 c3-2 nehemiah c7 esther i686 pentiumpro pentium2 pentium3 pentium3m pentium-m pentium4 pentium4m prescott nocona core2 nehalem corei7 westmere sandybridge corei7-avx ivybridge core-avx-i haswell core-avx2 broadwell skylake skylake-avx512 cannonlake icelake-client rocketlake icelake-server cascadelake tigerlake cooperlake sapphirerapids alderlake bonnell atom silvermont slm goldmont goldmont-plus tremont knl knm intel geode k6 k6-2 k6-3 athlon athlon-tbird athlon-4 athlon-xp athlon-mp x86-64 x86-64-v2 x86-64-v3 x86-64-v4 eden-x2 nano nano-1000 nano-2000 nano-3000 nano-x2 eden-x4 nano-x4 k8 k8-sse3 opteron opteron-sse3 athlon64 athlon64-sse3 athlon-fx amdfam10 barcelona bdver1 bdver2 bdver3 bdver4 znver1 znver2 znver3 btver1 btver2 generic native
Known valid arguments for -mtune= option:
generic i386 i486 pentium lakemont pentiumpro pentium4 nocona core2 nehalem sandybridge haswell bonnell silvermont goldmont goldmont-plus tremont knl knm skylake skylake-avx512 cannonlake icelake-client icelake-server cascadelake tigerlake cooperlake sapphirerapids alderlake rocketlake intel geode k6 athlon k8 amdfam10 bdver1 bdver2 bdver3 bdver4 btver1 btver2 znver1 znver2 znver3
Я также попробовал clang, и в обоих случаях циклы векторизованы, я полагаю, 32-байтовыми векторами:
remark: vectorized loop (vectorization width: 4, interleaved count: 4)
Я использую gcc 11.2.0
Редактировать: По просьбе Питера Кордеса Я понял, что какое-то время проводил бенчмаркинг с умножением на 4.
Makefile:
all:
gcc -O3 -mavx2 main.c -o 3
gcc -O3 -march=znver2 main.c -o 32
gcc -O3 -march=znver2 main.c -mprefer-vector-width=128 -o 32128
gcc -O3 -march=znver1 main.c -o 31
gcc -O2 -mavx2 main.c -o 2
gcc -O2 -march=znver2 main.c -o 22
gcc -O2 -march=znver2 main.c -mprefer-vector-width=128 -o 22128
gcc -O2 -march=znver1 main.c -o 21
hyperfine -r5 ./3 ./32 ./32128 ./31 ./2 ./22 ./22128 ./21
clean:
rm ./3 ./32 ./32128 ./31 ./2 ./22 ./22128 ./21
Код:
#include <stdio.h>
#include <inttypes.h>
#include <stdlib.h>
#include <time.h>
int main() {
const size_t N = 500;
uint64_t arr[N];
for (size_t i = 0; i < N; ++i)
arr[i] = 1;
for (int j = 0; j < 20000000; ++j)
for (size_t i = 0; i < N; ++i)
arr[i] *= 4;
srand(time(0));
printf("%lu\n", arr[rand() % N]); // use the array so that it is not optimized away
}
N = 500, arr[i] *= 4
:
Benchmark 1: ./3
Time (mean ± σ): 1.780 s ± 0.011 s [User: 1.778 s, System: 0.000 s]
Range (min … max): 1.763 s … 1.791 s 5 runs
Benchmark 2: ./32
Time (mean ± σ): 1.785 s ± 0.016 s [User: 1.783 s, System: 0.000 s]
Range (min … max): 1.773 s … 1.810 s 5 runs
Benchmark 3: ./32128
Time (mean ± σ): 1.740 s ± 0.026 s [User: 1.737 s, System: 0.000 s]
Range (min … max): 1.724 s … 1.785 s 5 runs
Benchmark 4: ./31
Time (mean ± σ): 1.757 s ± 0.022 s [User: 1.754 s, System: 0.000 s]
Range (min … max): 1.727 s … 1.785 s 5 runs
Benchmark 5: ./2
Time (mean ± σ): 3.467 s ± 0.031 s [User: 3.462 s, System: 0.000 s]
Range (min … max): 3.443 s … 3.519 s 5 runs
Benchmark 6: ./22
Time (mean ± σ): 3.475 s ± 0.028 s [User: 3.469 s, System: 0.001 s]
Range (min … max): 3.447 s … 3.512 s 5 runs
Benchmark 7: ./22128
Time (mean ± σ): 3.464 s ± 0.034 s [User: 3.459 s, System: 0.001 s]
Range (min … max): 3.431 s … 3.509 s 5 runs
Benchmark 8: ./21
Time (mean ± σ): 3.465 s ± 0.013 s [User: 3.460 s, System: 0.001 s]
Range (min … max): 3.443 s … 3.475 s 5 runs
N = 500, arr[i] *= 5
:
Benchmark 1: ./3
Time (mean ± σ): 1.789 s ± 0.004 s [User: 1.786 s, System: 0.001 s]
Range (min … max): 1.783 s … 1.793 s 5 runs
Benchmark 2: ./32
Time (mean ± σ): 1.772 s ± 0.017 s [User: 1.769 s, System: 0.000 s]
Range (min … max): 1.755 s … 1.800 s 5 runs
Benchmark 3: ./32128
Time (mean ± σ): 2.911 s ± 0.023 s [User: 2.907 s, System: 0.001 s]
Range (min … max): 2.880 s … 2.943 s 5 runs
Benchmark 4: ./31
Time (mean ± σ): 2.924 s ± 0.013 s [User: 2.921 s, System: 0.000 s]
Range (min … max): 2.906 s … 2.934 s 5 runs
Benchmark 5: ./2
Time (mean ± σ): 3.850 s ± 0.029 s [User: 3.846 s, System: 0.000 s]
Range (min … max): 3.823 s … 3.896 s 5 runs
Benchmark 6: ./22
Time (mean ± σ): 3.816 s ± 0.036 s [User: 3.812 s, System: 0.000 s]
Range (min … max): 3.777 s … 3.855 s 5 runs
Benchmark 7: ./22128
Time (mean ± σ): 3.813 s ± 0.026 s [User: 3.809 s, System: 0.000 s]
Range (min … max): 3.780 s … 3.834 s 5 runs
Benchmark 8: ./21
Time (mean ± σ): 3.783 s ± 0.010 s [User: 3.779 s, System: 0.000 s]
Range (min … max): 3.773 s … 3.798 s 5 runs
N = 512, arr[i] *= 4
Benchmark 1: ./3
Time (mean ± σ): 1.849 s ± 0.015 s [User: 1.847 s, System: 0.000 s]
Range (min … max): 1.831 s … 1.873 s 5 runs
Benchmark 2: ./32
Time (mean ± σ): 1.846 s ± 0.013 s [User: 1.844 s, System: 0.001 s]
Range (min … max): 1.832 s … 1.860 s 5 runs
Benchmark 3: ./32128
Time (mean ± σ): 1.756 s ± 0.012 s [User: 1.754 s, System: 0.000 s]
Range (min … max): 1.744 s … 1.771 s 5 runs
Benchmark 4: ./31
Time (mean ± σ): 1.788 s ± 0.012 s [User: 1.785 s, System: 0.001 s]
Range (min … max): 1.774 s … 1.801 s 5 runs
Benchmark 5: ./2
Time (mean ± σ): 3.476 s ± 0.015 s [User: 3.472 s, System: 0.001 s]
Range (min … max): 3.458 s … 3.494 s 5 runs
Benchmark 6: ./22
Time (mean ± σ): 3.449 s ± 0.002 s [User: 3.446 s, System: 0.000 s]
Range (min … max): 3.446 s … 3.452 s 5 runs
Benchmark 7: ./22128
Time (mean ± σ): 3.456 s ± 0.007 s [User: 3.453 s, System: 0.000 s]
Range (min … max): 3.446 s … 3.462 s 5 runs
Benchmark 8: ./21
Time (mean ± σ): 3.547 s ± 0.044 s [User: 3.542 s, System: 0.001 s]
Range (min … max): 3.482 s … 3.600 s 5 runs
N = 512, arr[i] *= 5
Benchmark 1: ./3
Time (mean ± σ): 1.847 s ± 0.013 s [User: 1.845 s, System: 0.000 s]
Range (min … max): 1.836 s … 1.863 s 5 runs
Benchmark 2: ./32
Time (mean ± σ): 1.830 s ± 0.007 s [User: 1.827 s, System: 0.001 s]
Range (min … max): 1.820 s … 1.837 s 5 runs
Benchmark 3: ./32128
Time (mean ± σ): 2.983 s ± 0.017 s [User: 2.980 s, System: 0.000 s]
Range (min … max): 2.966 s … 3.012 s 5 runs
Benchmark 4: ./31
Time (mean ± σ): 3.026 s ± 0.039 s [User: 3.021 s, System: 0.001 s]
Range (min … max): 2.989 s … 3.089 s 5 runs
Benchmark 5: ./2
Time (mean ± σ): 4.000 s ± 0.021 s [User: 3.994 s, System: 0.001 s]
Range (min … max): 3.982 s … 4.035 s 5 runs
Benchmark 6: ./22
Time (mean ± σ): 3.940 s ± 0.041 s [User: 3.934 s, System: 0.001 s]
Range (min … max): 3.890 s … 3.981 s 5 runs
Benchmark 7: ./22128
Time (mean ± σ): 3.928 s ± 0.032 s [User: 3.922 s, System: 0.001 s]
Range (min … max): 3.898 s … 3.979 s 5 runs
Benchmark 8: ./21
Time (mean ± σ): 3.908 s ± 0.029 s [User: 3.904 s, System: 0.000 s]
Range (min … max): 3.879 s … 3.954 s 5 runs
Я думаю, что запуск, где -O2 -march=znver1
был такой же скоростью, как -O3 -march=znver1
, был моей ошибкой с именованием файлов, я тогда еще не создал makefile, я использовал историю своей оболочки.
Интересно, что для случая *=4
(который он будет векторизовать для Zen1) -O3 -march=znver2
(256-битные векторы) был несколько медленнее, чем -O3 -march=znver1
(128-битные векторы), что, по-видимому, подтверждает выбор настройки GCC. Восстанавливает ли _Alignas(32)
в массиве производительность для случая -O3 -march=znver2
*=4
? Если это так, это будет означать, что неправильно выровненные 32-байтовые хранилища (и, возможно, даже загрузки) имеют дополнительные затраты на Zen1, даже если каждая половина выровнена по 16 байтам. Я надеялся, что это может быть не так, поскольку он запускает их как отдельные юопы, в отличие от Sandybridge, где один и тот же юоп использует AGU один раз.
Ничего не изменилось, тестировал с N=500 и N=512.
Спасибо за проверку; Я ожидал, что эффект «развертки цикла» 256-битных векторов на Zen1, по крайней мере, не повредит, но, по-видимому, это немного.
По умолчанию -mtune=generic
имеет -mprefer-vector-width=256
, и -mavx2
не меняет этого.
znver1 подразумевает -mprefer-vector-width=128
, потому что это вся нативная ширина HW. Инструкция, использующая 32-байтовые векторы YMM, декодируется как минимум до 2 мопов, больше, если это перетасовка с пересечением дорожки. Для простого вертикального SIMD вроде этого подойдут 32-байтовые векторы; конвейер эффективно обрабатывает инструкции 2-uop. (И я думаю, что ширина составляет 6 мкп, но только 5 инструкций, поэтому максимальная пропускная способность интерфейса недоступна при использовании только инструкций 1 мкп). Но когда векторизация потребует перетасовки, например. с массивами разной ширины элементов код-генерация GCC может стать более запутанной с 256-битным или более широким.
А vmovdqa ymm0, ymm1
mov-elimination работает только на младшей 128-битной половине на Zen1. Кроме того, обычное использование 256-битных векторов подразумевает, что после этого следует использовать vzeroupper
, чтобы избежать проблем с производительностью на других процессорах (но не на Zen1).
Я не знаю, как Zen1 обрабатывает смещенные 32-байтовые загрузки/сохранения, где каждая 16-байтовая половина выровнена, но в отдельных строках кэша. Если это работает хорошо, GCC может рассмотреть возможность увеличения znver1 -mprefer-vector-width
до 256. Но более широкие векторы означают больше очищающего кода, если неизвестно, что размер кратен ширине вектора.
В идеале GCC мог бы обнаруживать подобные случаи легко и использовать там 256-битные векторы. (Чистая вертикаль, отсутствие смешивания ширины элементов, постоянный размер, кратный 32 байтам.) По крайней мере, на ЦП, где это нормально: znver1, но не bdver2, например, где 256-битные хранилища всегда медленны из-за ошибки проектирования ЦП.
Вы можете увидеть результат этого выбора в том, как он векторизует ваш первый цикл, похожий на memset, с помощью vmovdqu [rdx], xmm0
. https://godbolt.org/z/E5Tq7Gfzc
Итак, учитывая, что GCC решил использовать только 128-битные векторы, которые могут содержать только два элемента uint64_t
, он (правильно или неправильно) решает, что не стоит использовать vpsllq
/ vpaddd
для реализации qword *5
как (v<<2) + v
, а не делать это с целым числом в одной инструкции LEA.
В этом случае почти наверняка это неправильно, поскольку для каждого элемента или пары элементов по-прежнему требуется отдельная загрузка и сохранение. (И накладные расходы на цикл, поскольку по умолчанию GCC не разворачивается, кроме как с PGO, -fprofile-use
. SIMD похож на развертывание цикла, особенно на ЦП, который обрабатывает 256-битные векторы как 2 отдельных мопов.)
Я не совсем уверен, что GCC подразумевает под «не векторизованным: неподдерживаемый тип данных». x86 не имеет инструкции умножения SIMD uint64_t
до AVX-512, поэтому, возможно, GCC назначает ей стоимость на основе общий случай из-за необходимости эмулировать ее с несколькими 32x32 => 64-битными pmuludq
инструкциями и кучей перетасовок. И только после того, как он преодолеет этот горб, он поймет, что на самом деле это довольно дешево для такой константы, как 5
, всего с двумя установленными битами?
Это объяснило бы здесь процесс принятия решений GCC, но я не уверен, что это точно правильное объяснение. Тем не менее, такие факторы происходят в таком сложном механизме, как компилятор. Опытный человек может легко сделать более разумный выбор, но компиляторы просто выполняют последовательности проходов оптимизации, которые не всегда учитывают общую картину и все детали одновременно.
-mprefer-vector-width=256
не помогает:uint64_t *= 5
кажется регрессией GCC9(Эталонные тесты в вопросе подтверждают, что фактический процессор Zen1 получает ускорение почти в 2 раза, как и ожидалось, при выполнении 2x uint64 за 6 операций в секунду по сравнению с 1x за 5 операций со скаляром. Или 4x uint64_t за 10 операций с 256-битными векторами, включая два 128-битные хранилища, которые будут узким местом пропускной способности наряду с внешним интерфейсом.)
Даже с -march=znver1 -O3 -mprefer-vector-width=256
мы не получаем цикл *= 5
, векторизованный с помощью GCC9, 10 или 11 или текущего ствола. Как вы говорите, мы делаем с -march=znver2
. https://godbolt.org/z/dMTh7Wxcq
Мы получаем векторизацию с этими параметрами для uint32_t
(даже оставляя ширину вектора 128-битной). Скаляр будет стоить 4 операции на вектор uop (не инструкцию), независимо от 128 или 256-битной векторизации на Zen1, поэтому это не говорит нам, является ли *=
тем, что заставляет модель стоимости решить не векторизовать, или только 2 против , 4 элемента на 128-битную внутреннюю МОП.
С uint64_t
изменение на arr[i] += arr[i]<<2;
по-прежнему не векторизируется, но arr[i] <<= 1;
делает. (https://godbolt.org/z/6PMn93Y5G). Даже arr[i] <<= 2;
и arr[i] += 123
в одном и том же цикле векторизуются по тем же инструкциям, которые, по мнению GCC, не стоят того для векторизации *= 5
, просто разные операнды, константы вместо исходного вектора снова. (Скаляр все еще может использовать один LEA). Таким образом, очевидно, что стоимостная модель не выглядит так далеко, как окончательные машинные инструкции x86 asm, но я не знаю, почему arr[i] += arr[i]
считается более дорогим, чем arr[i] <<= 1;
, что в точности одно и то же.
GCC8 векторизует ваш цикл даже при 128-битной ширине вектора:https://godbolt.org/z/5o6qjc7f6
# GCC8.5 -march=znver1 -O3 (-mprefer-vector-width=128)
.L12: # do{
vmovups xmm1, XMMWORD PTR [rsi] # 16-byte load
add rsi, 16 # ptr += 2 elements
vpsllq xmm0, xmm1, 2 # v << 2
vpaddq xmm0, xmm0, xmm1 # tmp += v
vmovups XMMWORD PTR [rsi-16], xmm0 # store
cmp rax, rsi
jne .L12 # } while(p != endp)
С -march=znver1 -mprefer-vector-width=256
выполнение хранилища в виде двух 16-байтовых половин с vmovups xmm
/ vextracti128
равно Почему gcc не разрешает _mm256_loadu_pd как одиночный vmovupd? znver1 подразумевает -mavx256-split-unaligned-store
(что влияет на каждое хранилище, когда GCC не знаю наверняка, что оно выровнено. Таким образом, требуются дополнительные инструкции, даже если данные действительно выровнены ).
Однако znver1 не подразумевает -mavx256-split-unaligned-load
, поэтому GCC готов складывать загрузки в качестве операндов источника памяти в операции ALU в коде, где это полезно.
Если я перезапишу -mprefer-vector-width=256
после -march=znver1
, то первый цикл векторизуется 32-байтовыми векторами, а второй — никак. Я так понимаю, причина в другом? Однако он не может векторизоваться только с uint64_t, так что, вероятно, это связано?
Это также удастся, если я создам отдельный массив, заполню его пятерками и перемножу их вместе.
@TheHardew: Хм. Я предполагал, что это сработает, но не проверял. Да, почти наверняка из-за 64-битных элементов. Вы говорите, что он векторизует uint64_t
размножение? Можете ли вы связать это с Godbolt? Это на самом деле быстрее, как постоянное распространение 1 и/или 5, или это работает так, как Самый быстрый способ умножить массив int64_t? показывает обработку общего случая?
@TheHardew: я проверил себя и обновил свой ответ. GCC11.2 на Godbolt делает векторизовать с помощью -march=znver1 -O3 -mprefer-vector-width=256
. Порядок этих опций, похоже, не имеет значения.
Вы также изменили тип на uint32_t
. С uint64_t
и -mprefer-vector-width=256
он не векторизуется с -march=znver1
, даже если я скомпилирую код только с -mavx2
, мой процессор сможет запустить векторизованный код.
«Вы говорите, что он векторизует uint64_t многократно?» Извините, я пропустил это. Нет, uint64_t не vecotrize. Кроме того, тестирование на ryzen 1700x показало, что обе версии примерно равны по скорости.
@TheHardew: Извините, вы правы, это регрессия в GCC9 и более поздних версиях по сравнению с GCC8, который векторизует этот случай (даже со 128-битными векторами для 64-битного размера). godbolt.org/z/1766W8T79 . Я забыл, что играл с uint32_t, когда выбрал ссылку Godbolt для своего ответа. Кстати, вам не нужно писать целую программу, просто функцию, чтобы вы могли смотреть на меньшее количество ассемблерного кода.
Итак, я принял этот ответ, я думаю, вы правы в том, что это регрессия, или, может быть, они просто решили, что, поскольку он работает так же быстро, как не векторизованная версия, он того не стоит. Спасибо за помощь.
@TheHardew: Вы сравнивали его с этим крошечным тестом, который повторяет только один раз более 1000 элементов и тратит большую часть времени на печать? Надеюсь, что нет, но ты не сказал. Идиоматический способ оценки производительности?
Я увеличил N до 1 000 000, зациклил второй цикл 10 тысяч раз, убедился, что он векторизован, и напечатал случайный элемент. Я использовал сверхтонкий, чтобы запустить программу 10 раз и измерить среднее время.
@TheHardew: Хорошо, это звучит разумно, хотя увеличение размера означает, что теперь вы тестируете пропускную способность L3, а не пропускную способность ALU / внешнего интерфейса. Размер, который подходит для L1d, хорош. Если вы можете подтвердить ускорение с большим размером (например, используя -march=znver2
с/без -mprefer-vector-width=128
, чтобы доказать, что 128-битная векторизация этого цикла является выигрышем для вашего Zen1), вы можете сообщить об ошибке пропущенной оптимизации на gcc.gnu.org/bugzilla.
@TheHardew: обновлен последний раздел, чтобы исправить путаницу uint32 и 64. Я нашел несколько случаев, когда GCC11 был готов автоматически векторизовать операции uint64_t
, такие как <<= 1
для znver1 (ширина вектора не имела значения).
znver2 256 векторов на 2,13(62) % быстрее, znver2 128 битных векторов на 0,85(63) % быстрее. Думаю, я попытаюсь сообщить об этом позже.
@TheHardew: Разница всего в пару процентов? Да, я думал, что это будет ближе к коэффициенту два для многократного зацикливания массива от 8 до 16 КБ, узкого места на пропускной способности внешнего интерфейса или порта хранилища. (1x 16 или 8-байтовое хранилище за такт с достаточной разверткой цикла.)
@TheHardew: О, это все еще из вашего теста со слишком большими массивами, поэтому у вас узкое место на пропускной способности L3? Детали того, как вычисляет эталонная программа, не будут разницей между 2% и 2x ускорением.
Я перезапустил его сейчас с массивом 4 КиБ, но я компенсировал это, зациклив больше раз. Пока резкого ускорения нет. Кроме того, в этом удаленном комментарии я имел в виду только то, что неопределенности, связанные с процентным ускорением, могут быть неправильными, но я понял, что он, вероятно, правильно вычисляет стандартное отклонение, я просто посмотрел на неправильную часть кода.
@TheHardew: Может быть, попробовать с _Alignas(32)
в массиве, на случай, если смещенные 32-байтные хранилища замедляют работу? Мне также любопытно, может ли этот векторный цикл из 6 операций (со 128-битными векторами) работать неэффективно на 5-канальном конвейере, как версия AMD Снижается ли производительность при выполнении циклов, число операций которых не кратно ширине процессора?. Но ваш тест с -march=znver2
с 256-битными векторами, вероятно, исключает это. Это будет 10 мкп для Zen1, как развертывание цикла.
Это ничего не меняет. Кроме того, я понял, что совершил ошибку. Массив теперь 4000 Б. С 4096 Б, 256-битный znver2 и универсальный mavx2 фактически замедляются на 1% (5 стандартных отклонений). Но не zenvr2 128 бит, а znver1.
@TheHardew: Интересно. Я думаю, тогда эвристика настройки GCC принимает правильное решение. (Особенно для случаев, когда известно, что N не было кратно 4 или 8, поэтому для векторной версии потребуется дополнительная очистка)
Кроме того, оба случая znver2
, -mavx2
в два раза медленнее с -O2
, но znver1
имеет одинаковую скорость.
@TheHardew: Что?? Таким образом, автовекторизация (включенная в -O3, но не в -O2) делает вашу программу в два раза быстрее с -march=znver2
при работе на вашем процессоре Zen1? Вы повторяете цикл инициализации = 1
, а также цикл *= 5
внутри своего теста? Теперь я не уверен, что вы сравнивали с тем, что раньше, поскольку -O3 -march=znver2 -mprefer-vector-width=128
против znver1
должен был быть тот же тест SIMD против скалярного (соответственно), который, как вы сказали, не давал ускорения на вашем Zen1. Возможно, отредактируйте свой вопрос, указав некоторые подробности о том, что именно вы тестировали, и каково было абсолютное время.
Я обновил пост, я допустил некоторые ошибки при написании комментариев, например, я изменил *5
на *4
. Кажется, действительно есть ошибка в gcc
на znver1
. Извините, что отнял у вас столько времени, и я очень благодарен вам за помощь.
-O2
не включает-ftree-vectorize
(до GCC12), поэтому неудивительно, что все-O2
результаты примерно одинаковы независимо от-march
вариантов. Чтобы еще больше минимизировать накладные расходы, вы можете закончить main с помощьюreturn arr[argc];
или назначить его наvolatile uint64_t
. Насколько известно компилятору, это все еще любой элемент, и он не делает дополнительных системных вызовов, особенно не печатает на ваш терминал. Это нормально, но если вы собираетесь отправить отчет об ошибке с пропущенной оптимизацией в gcc.gnu.org/bugzilla, вы можете исправить это таким образом. Конечноsrand
не нужно.