Почему gcc -march=znver1 ограничивает векторизацию uint64_t?

Я пытаюсь убедиться, что 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, я использовал историю своей оболочки.

-O2 не включает -ftree-vectorize (до GCC12), поэтому неудивительно, что все -O2 результаты примерно одинаковы независимо от -march вариантов. Чтобы еще больше минимизировать накладные расходы, вы можете закончить main с помощью return arr[argc]; или назначить его на volatile uint64_t. Насколько известно компилятору, это все еще любой элемент, и он не делает дополнительных системных вызовов, особенно не печатает на ваш терминал. Это нормально, но если вы собираетесь отправить отчет об ошибке с пропущенной оптимизацией в gcc.gnu.org/bugzilla, вы можете исправить это таким образом. Конечно srand не нужно.
Peter Cordes 10.04.2022 04:49

Интересно, что для случая *=4 (который он будет векторизовать для Zen1) -O3 -march=znver2 (256-битные векторы) был несколько медленнее, чем -O3 -march=znver1 (128-битные векторы), что, по-видимому, подтверждает выбор настройки GCC. Восстанавливает ли _Alignas(32) в массиве производительность для случая -O3 -march=znver2*=4? Если это так, это будет означать, что неправильно выровненные 32-байтовые хранилища (и, возможно, даже загрузки) имеют дополнительные затраты на Zen1, даже если каждая половина выровнена по 16 байтам. Я надеялся, что это может быть не так, поскольку он запускает их как отдельные юопы, в отличие от Sandybridge, где один и тот же юоп использует AGU один раз.

Peter Cordes 10.04.2022 04:56

Ничего не изменилось, тестировал с N=500 и N=512.

TheHardew 10.04.2022 05:24

Спасибо за проверку; Я ожидал, что эффект «развертки цикла» 256-битных векторов на Zen1, по крайней мере, не повредит, но, по-видимому, это немного.

Peter Cordes 10.04.2022 05:26
Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Будучи разработчиком веб-приложений, легко впасть в заблуждение, считая, что приложение без JavaScript не имеет права на жизнь. Нам становится удобно...
Flatpickr: простой модуль календаря для вашего приложения на React
Flatpickr: простой модуль календаря для вашего приложения на React
Если вы ищете пакет для быстрой интеграции календаря с выбором даты в ваше приложения, то библиотека Flatpickr отлично справится с этой задачей....
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Клиент для URL-адресов, cURL, позволяет взаимодействовать с множеством различных серверов по множеству различных протоколов с синтаксисом URL.
Четыре эффективных способа центрирования блочных элементов в CSS
Четыре эффективных способа центрирования блочных элементов в CSS
У каждого из нас бывали случаи, когда нам нужно отцентрировать блочный элемент, но мы не знаем, как это сделать. Даже если мы реализуем какой-то...
3
4
64
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

По умолчанию -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 09.04.2022 23:58

Это также удастся, если я создам отдельный массив, заполню его пятерками и перемножу их вместе.

TheHardew 10.04.2022 00:09

@TheHardew: Хм. Я предполагал, что это сработает, но не проверял. Да, почти наверняка из-за 64-битных элементов. Вы говорите, что он векторизует uint64_t размножение? Можете ли вы связать это с Godbolt? Это на самом деле быстрее, как постоянное распространение 1 и/или 5, или это работает так, как Самый быстрый способ умножить массив int64_t? показывает обработку общего случая?

Peter Cordes 10.04.2022 00:09

@TheHardew: я проверил себя и обновил свой ответ. GCC11.2 на Godbolt делает векторизовать с помощью -march=znver1 -O3 -mprefer-vector-width=256. Порядок этих опций, похоже, не имеет значения.

Peter Cordes 10.04.2022 00:21

Вы также изменили тип на uint32_t. С uint64_t и -mprefer-vector-width=256 он не векторизуется с -march=znver1, даже если я скомпилирую код только с -mavx2, мой процессор сможет запустить векторизованный код.

TheHardew 10.04.2022 01:07

«Вы говорите, что он векторизует uint64_t многократно?» Извините, я пропустил это. Нет, uint64_t не vecotrize. Кроме того, тестирование на ryzen 1700x показало, что обе версии примерно равны по скорости.

TheHardew 10.04.2022 01:50

@TheHardew: Извините, вы правы, это регрессия в GCC9 и более поздних версиях по сравнению с GCC8, который векторизует этот случай (даже со 128-битными векторами для 64-битного размера). godbolt.org/z/1766W8T79 . Я забыл, что играл с uint32_t, когда выбрал ссылку Godbolt для своего ответа. Кстати, вам не нужно писать целую программу, просто функцию, чтобы вы могли смотреть на меньшее количество ассемблерного кода.

Peter Cordes 10.04.2022 01:51

Итак, я принял этот ответ, я думаю, вы правы в том, что это регрессия, или, может быть, они просто решили, что, поскольку он работает так же быстро, как не векторизованная версия, он того не стоит. Спасибо за помощь.

TheHardew 10.04.2022 01:57

@TheHardew: Вы сравнивали его с этим крошечным тестом, который повторяет только один раз более 1000 элементов и тратит большую часть времени на печать? Надеюсь, что нет, но ты не сказал. Идиоматический способ оценки производительности?

Peter Cordes 10.04.2022 01:59

Я увеличил N до 1 000 000, зациклил второй цикл 10 тысяч раз, убедился, что он векторизован, и напечатал случайный элемент. Я использовал сверхтонкий, чтобы запустить программу 10 раз и измерить среднее время.

TheHardew 10.04.2022 02:05

@TheHardew: Хорошо, это звучит разумно, хотя увеличение размера означает, что теперь вы тестируете пропускную способность L3, а не пропускную способность ALU / внешнего интерфейса. Размер, который подходит для L1d, хорош. Если вы можете подтвердить ускорение с большим размером (например, используя -march=znver2 с/без -mprefer-vector-width=128, чтобы доказать, что 128-битная векторизация этого цикла является выигрышем для вашего Zen1), вы можете сообщить об ошибке пропущенной оптимизации на gcc.gnu.org/bugzilla.

Peter Cordes 10.04.2022 02:07

@TheHardew: обновлен последний раздел, чтобы исправить путаницу uint32 и 64. Я нашел несколько случаев, когда GCC11 был готов автоматически векторизовать операции uint64_t, такие как <<= 1 для znver1 (ширина вектора не имела значения).

Peter Cordes 10.04.2022 02:29

znver2 256 векторов на 2,13(62) % быстрее, znver2 128 битных векторов на 0,85(63) % быстрее. Думаю, я попытаюсь сообщить об этом позже.

TheHardew 10.04.2022 02:30

@TheHardew: Разница всего в пару процентов? Да, я думал, что это будет ближе к коэффициенту два для многократного зацикливания массива от 8 до 16 КБ, узкого места на пропускной способности внешнего интерфейса или порта хранилища. (1x 16 или 8-байтовое хранилище за такт с достаточной разверткой цикла.)

Peter Cordes 10.04.2022 02:31

@TheHardew: О, это все еще из вашего теста со слишком большими массивами, поэтому у вас узкое место на пропускной способности L3? Детали того, как вычисляет эталонная программа, не будут разницей между 2% и 2x ускорением.

Peter Cordes 10.04.2022 02:35

Я перезапустил его сейчас с массивом 4 КиБ, но я компенсировал это, зациклив больше раз. Пока резкого ускорения нет. Кроме того, в этом удаленном комментарии я имел в виду только то, что неопределенности, связанные с процентным ускорением, могут быть неправильными, но я понял, что он, вероятно, правильно вычисляет стандартное отклонение, я просто посмотрел на неправильную часть кода.

TheHardew 10.04.2022 02:46

@TheHardew: Может быть, попробовать с _Alignas(32) в массиве, на случай, если смещенные 32-байтные хранилища замедляют работу? Мне также любопытно, может ли этот векторный цикл из 6 операций (со 128-битными векторами) работать неэффективно на 5-канальном конвейере, как версия AMD Снижается ли производительность при выполнении циклов, число операций которых не кратно ширине процессора?. Но ваш тест с -march=znver2 с 256-битными векторами, вероятно, исключает это. Это будет 10 мкп для Zen1, как развертывание цикла.

Peter Cordes 10.04.2022 02:53

Это ничего не меняет. Кроме того, я понял, что совершил ошибку. Массив теперь 4000 Б. С 4096 Б, 256-битный znver2 и универсальный mavx2 фактически замедляются на 1% (5 стандартных отклонений). Но не zenvr2 128 бит, а znver1.

TheHardew 10.04.2022 03:07

@TheHardew: Интересно. Я думаю, тогда эвристика настройки GCC принимает правильное решение. (Особенно для случаев, когда известно, что N не было кратно 4 или 8, поэтому для векторной версии потребуется дополнительная очистка)

Peter Cordes 10.04.2022 03:08

Кроме того, оба случая znver2, -mavx2 в два раза медленнее с -O2, но znver1 имеет одинаковую скорость.

TheHardew 10.04.2022 03:12

@TheHardew: Что?? Таким образом, автовекторизация (включенная в -O3, но не в -O2) делает вашу программу в два раза быстрее с -march=znver2 при работе на вашем процессоре Zen1? Вы повторяете цикл инициализации = 1, а также цикл *= 5 внутри своего теста? Теперь я не уверен, что вы сравнивали с тем, что раньше, поскольку -O3 -march=znver2 -mprefer-vector-width=128 против znver1 должен был быть тот же тест SIMD против скалярного (соответственно), который, как вы сказали, не давал ускорения на вашем Zen1. Возможно, отредактируйте свой вопрос, указав некоторые подробности о том, что именно вы тестировали, и каково было абсолютное время.

Peter Cordes 10.04.2022 03:17

Я обновил пост, я допустил некоторые ошибки при написании комментариев, например, я изменил *5 на *4. Кажется, действительно есть ошибка в gcc на znver1. Извините, что отнял у вас столько времени, и я очень благодарен вам за помощь.

TheHardew 10.04.2022 04:05

Другие вопросы по теме