Není tam chyba?
int idx = i/2 + imod - 3;
//vs:
outbuf2[i2 + 1] = (float)raw3;
IMHO v tom prvním má být imod-2
. Nezkoušel jsem, jestli to nějak zlepší výsledek autovektorizace.
Ohledně poznámky výše a SSE vs aarch64 - na Apple M1 netřeba, stačí zkusit Graviton3 v AWS - podporuje Neon i SVE. S těmi 2+2 konverzemi si nejsem jistý, jestli to zvládne efektivně. Ještě by mělo smysl zkusit AVX-512, které je taky už nějakou dobu standard, aspoň u Intel.
PS: Pěkný článek a hezké ne zcela běžné využití SIMD.
Jen doplním, že M1 má "plnotučná" jádra, která si poradí i s neoptimálním kódem (podobně jako x86 jádra od Intel a AMD). Zatímco vše ostatní jsou jádra Cortex, která tak chytrá (ale taky velká) nejsou. Příkladem jsou optimalizace, které nemají na M1 vliv (neoptimální kód tam běží stejně rychle jako optimální). Samozřejmě pro Amazon a cloud obecně je lepší více menších hloupějších jader a optimalizovat software na míru (variable vs fixed costs).
Je pravda, že u násobení matic mi přišly třeba Graviton na Neon pomalejší než M1, se SVE ale byly rychlejší.
Nicméně, zkusil jsem pro zajímavost M1 a Graviton3:
#include <stdint.h> #include <stdlib.h> #ifdef __aarch64__ #include <arm_neon.h> #endif void radar_ref(float *outbuf1, float *outbuf2, int16_t *inbuf, int SAMPLES) { for (size_t i = 0; i<4*SAMPLES; i++) { // fix sign-extend int16_t raw = inbuf[i]; raw = (raw & 0xEFFF) | ((raw & 0xE000)>>1); // make float from int16 float raw_f = (float)raw; // decide where to put it - horizontal or vertical buffer int imod = i % 4; if(imod < 2) { int idx = i/2 + imod; outbuf1[idx] = raw_f; } else { int idx = i/2 + imod - 3; outbuf2[idx] = raw_f; } } } void radar_Neon(float *outbuf1, float *outbuf2, int16_t *inbuf, int SAMPLES) { int16x4_t four16_EFFF = vdup_n_s16(0xEFFF); int16x4_t four16_E000 = vdup_n_s16(0xE000); for (size_t b = 0; b < SAMPLES; b += 1) { // fix sign-extend int16x4_t raw4 = ((int16x4_t *)inbuf)[b]; int32x4_t adjusted = vmovl_s16(vorr_s16(vand_s16(raw4, four16_EFFF), vshr_n_s16(vand_s16(raw4, four16_E000), 1))); // make float from int16 float32x4_t flt = vcvtq_f32_s32(adjusted); vst1_f32(outbuf1+b*2, vget_low_f32(flt)); vst1_f32(outbuf2+b*2, vget_high_f32(flt)); } } // main.c : // (too smart compiler may completely remove code doing nothing) #include <stdint.h> #include <stdlib.h> extern void radar_ref(float *outbuf1, float *outbuf2, int16_t *inbuf, int SAMPLES); extern void radar_Neon(float *outbuf1, float *outbuf2, int16_t *inbuf, int SAMPLES); #define SAMPLES 30000 #define ITERATIONS 1000000 int main(void) { float outbuf1[SAMPLES*2], outbuf2[SAMPLES*2]; int16_t inbuf[SAMPLES*4]; for (int i = 0; i < ITERATIONS; ++i) { radar_ref(outbuf1, outbuf2, inbuf, SAMPLES); } return 0; }
Nejspíš by šlo vylepšit zpracováním osmi prvků najednou, podobně jako v původním AVX. Případně šestnácti prvků a využít load and store pair.
Neznám tak dobře shuffle instrukce na Neon, abych to dal z hlavy.
Apple M1 Pro (clang 14.0.0) :
Letmým pohled, clang byl schopen autovektorizovat aspoň operace po dvou prvcích.
ref: 44 us
Neon: 20 us
Graviton3 c7g.medium (gcc 12.1.0) :
Přímý kód, žádná autovektorizace.
ref: 183 us
Neon: 41 us