Mir ist aufgefallen, dass Sie __uint128_t
zu xxh3 hinzugefügt haben. Das ist nicht auf 32-Bit portierbar; der Typ ist nur für 64-Bit definiert, und wie Sie unten sehen, aus einem ziemlich vernünftigen Grund.
Ich schlage folgendes vor:
static U64 mult128(U64 bot, U64 top) {
#if (SIZE_MAX > 0xFFFFFFFFULL) || defined(__SIZEOF_UINT128__) /* TODO: better detection */
__uint128_t prod = (__uint128_t)bot * (__uint128_t)top;
return prod + (prod >> 64);
#else
/* based off of LLVM's optimization of https://github.com/calccrypto/uint128_t's
* operator* when both upper halves are zero.
* There's probably a better way to do this, and if it weren't for the
* mixing in the middle, it could be easily SIMD'd. */
U64 BD = (top & 0xFFFFFFFF) * (bot & 0xFFFFFFFF);
U64 AC = (top >> 32) * (bot >> 32);
U64 BC = (top & 0xFFFFFFFF) * (bot >> 32);
U64 AD = (top >> 32) * (bot & 0xFFFFFFFF);
U64 sum1 = (BC & 0xFFFFFFFF);
U64 sum2 = (AD & 0xFFFFFFFF);
sum1 += (BD >> 32);
sum2 += sum1;
sum1 = sum2 >> 32;
sum2 <<= 32;
sum1 += (BD & 0xFFFFFFFF);
sum2 += (AC & 0xFFFFFFFF);
sum1 += (BC >> 32);
sum2 += (AD >> 32);
sum1 += (AC & 0xFFFFFFFF00000000ULL);
sum2 += sum1;
printf("%llx\n", sum2);
return sum2;
}
#endif
}
Ich möchte jedoch erwähnen, dass GCC diesen Code nicht wirklich mag und ihn halb vektorisiert.
Ich probiere jedoch eine potenzielle SSE2/NEON32-Version aus, die möglicherweise schneller oder langsamer ist. Den ersten Teil habe ich von MSDN bekommen , aber der zweite Teil verwirrt mich.
uint32_t A = top >> 32;
uint32_t B = top & 0xFFFFFFFF;
uint32_t C = bot >> 32;
uint32_t D = bot & 0xFFFFFFFF;
__m128i ba = _mm_set_epi32(0, B, 0, A); // { B, A }
__m128i dc = _mm_set_epi32(0, D, 0, C); // { D, C }
__m128i cd = _mm_shuffle_epi32(dc, _MM_SHUFFLE(1, 0, 3, 2)); // { C, D }
__m128i bd_ac = _mm_mul_epu32(ba, dc); // { BD, AC }
__m128i bc_ad = _mm_mul_epu32(ba, cd); // { BC, AD }
// this could be improved probably
#ifdef __SSE4_1__
__m128i zero = _mm_setzero_si128();
__m128i bc_ad_lo = _mm_blend_epi16(bc_ad, zero, 0xCC); // bd_ac & 0xFFFFFFFF;
#else
__m128i ff = _mm_set_epi32(0, 0xFFFFFFFF, 0, 0xFFFFFFFF);
__m128i bc_ad_lo = _mm_and_si128(bc_ad, ff);
#endif
__m128i bd_hi = _mm_srli_si128(bd_ac, 12); // { 0, BD >> 32 }
__m128i sum = _mm_add_epi64(bc_ad_lo, bd_hi); // { bc & 0xFFFFFFFF, ad & 0xFFFFFFFF + BD >> 32 }
__m128i sumShuf = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(sum), _mm_setzero_ps(), _MM_SHUFFLE(0, 0, 3, 2)));
sum = _mm_add_epi64(sum, sumShuf);
// dunno
Wenn ich es mit Vektorerweiterungen mache, gibt mir Clang Folgendes:
U64x2 BA = { top >> 32, top & 0xFFFFFFFF };
U64x2 DC = { bot >> 32, bot & 0xFFFFFFFF };
U64x2 CD = { bot & 0xFFFFFFFF, bot >> 32 };
U64x2 BD_AC = (BA & 0xFFFFFFFF) * (DC & 0xFFFFFFFF);
U64x2 BC_AD = (BA & 0xFFFFFFFF) * (CD & 0xFFFFFFFF);
U64x2 sum = BC_AD & 0xFFFFFFFF;
sum[1] += BD_AC[0] >> 32;
sum[0] += sum[1];
U64x2 sumv2 = { sum[0] << 32, sum[0] >> 32 };
sum = sumv2;
sum += BD_AC & 0xFFFFFFFF;
sum += BC_AD >> 32;
sum[1] += (BD_AC[1] & 0xFFFFFFFF00000000);
sum[0] += sum[1];
return sum[0];
In der Mitte wird jedoch auf Skalar umgeschaltet. Ich wünschte irgendwie, ich hätte die halben Register von NEON…
_mull_u64_v2: ## <strong i="9">@mull_u64_v2</strong>
## %bb.0:
push esi
call L1$pb
L1$pb:
pop eax
movd xmm0, dword ptr [esp + 16] ## xmm0 = mem[0],zero,zero,zero
movd xmm2, dword ptr [esp + 20] ## xmm2 = mem[0],zero,zero,zero
punpcklqdq xmm2, xmm0 ## xmm2 = xmm2[0],xmm0[0]
movd xmm0, dword ptr [esp + 8] ## xmm0 = mem[0],zero,zero,zero
movd xmm3, dword ptr [esp + 12] ## xmm3 = mem[0],zero,zero,zero
movdqa xmm1, xmm3
punpcklqdq xmm1, xmm0 ## xmm1 = xmm1[0],xmm0[0]
punpcklqdq xmm0, xmm3 ## xmm0 = xmm0[0],xmm3[0]
pmuludq xmm1, xmm2
pmuludq xmm0, xmm2
pshufd xmm2, xmm1, 229 ## xmm2 = xmm1[1,1,2,3]
movd edx, xmm2
pshufd xmm2, xmm0, 78 ## xmm2 = xmm0[2,3,0,1]
movd esi, xmm2
xor ecx, ecx
add esi, edx
setb cl
movd edx, xmm0
add edx, esi
adc ecx, 0
movd xmm2, ecx
movd xmm3, edx
pshufd xmm4, xmm1, 231 ## xmm4 = xmm1[3,1,2,3]
pand xmm1, xmmword ptr [eax + LCPI1_0-L1$pb]
shufps xmm3, xmm2, 65 ## xmm3 = xmm3[1,0],xmm2[0,1]
psrlq xmm0, 32
paddq xmm0, xmm1
paddq xmm0, xmm3
movd eax, xmm4
pshufd xmm1, xmm0, 78 ## xmm1 = xmm0[2,3,0,1]
movd ecx, xmm1
pshufd xmm1, xmm0, 231 ## xmm1 = xmm0[3,1,2,3]
movd esi, xmm1
add esi, eax
pshufd xmm1, xmm0, 229 ## xmm1 = xmm0[1,1,2,3]
movd edx, xmm1
movd eax, xmm0
add eax, ecx
adc edx, esi
pop esi
ret
Ja, Sie haben recht @easyaspi314 , und dies ist ein bekanntes Problem.
Ich plane, einen dedizierten Code hinzuzufügen, um dies für 32-Bit-Plattformen (und Nicht-Gcc-Plattformen) zu emulieren.
Es sind bereits mehrere Implementierungen verfügbar, also sollte ich in der Lage sein, eine zu greifen und anzuschließen.
Eigentlich sieht der Skalar gar nicht so schlecht aus.
GCC hasst den Code immer noch und besteht darauf, den Stack zu verwenden (oder eine hässliche partielle Vektorisierung für den ersten), aber Clang gibt sauberen Code für alle aus. Es gibt zwar den besten Code für ARMv7 aus, aber das liegt hauptsächlich an den akkumulierten Anweisungen.
umaal
ist eine komplette Bestie einer Anweisung, die in ARMv6 hinzugefügt wurde:
void umaal(uint32_t *RdLo, uint32_t *RdHi, uint32_t Rn, uint32_t Rm)
{
uint64_t prod = (uint64_t) Rn * (uint64_t) Rm;
prod += (uint64_t) *RdLo;
prod += (uint64_t) *RdHi;
*RdLo = prod & 0xFFFFFFFF;
*RdHi = prod >> 32;
}
Hier ist ein Vergleich einiger Implementierungen, die ich online gefunden habe.
https://gcc.godbolt.org/z/pWh9No
Das zweite könnte besser sein, weil GCC es nicht vektorisiert und es auf ARM nicht schrecklich ist (es fügt nur eine zusätzliche Anweisung hinzu). Bei MSVC möchten wir dies intrinsisch statt umwandeln, da MSVC dumm ist und versuchen wird, eine vollständige 64-Bit-Multiplikation durchzuführen.
Ich habe den xxh3
Zweig mit einer neuen Version von xxh3.h
hochgeladen
die ein Update für die mul128
Funktion auf Nicht-x64-Plattformen enthält.
Ich könnte überprüfen, ob es im 32-Bit-Modus kompiliert und ordnungsgemäß ausgeführt wird.
Es enthält auch einen dedizierten Pfad für ARM aarch
, obwohl ich glaube, dass das vorhandene gcc
eine höhere Priorität hat und wahrscheinlich das gleiche Ergebnis liefern wird (die generierte Assembly wurde noch nicht überprüft). Vielleicht können Sie diesen Teil besser verstehen.
https://gcc.godbolt.org/z/PRtMJy
Boom. Geld.
37-48 Anweisungen auf x86, 8-9 Anweisungen auf ARMv6+, und dank __attribute__((__target__("no-sse2")))
vektorisiert GCC es nicht teilweise, wenn es für Core 2 optimiert wird.
Ich musste Inline-Assembly für ARM verwenden, weil ich Clang oder GCC nicht dazu bringen konnte, meine Anfrage für umaal
herauszufinden. Leider, ja, dieses Durcheinander von #if
Aussagen ist irgendwie scheiße.
umull r12, lr, r0, r2 @ {r12, lr} = (U64)r0 * (U64)r2
mov r5, #0 @ r5 = 0
mov r4, #0 @ r4 = 0
umaal r5, lr, r1, r2 @ {r5, lr} = ((U64)r1 * (U64)r2) + r5 + lr
umaal r5, r4, r0, r3 @ {r5, r4} = ((U64)r0 * (U64)r3) + r5 + r4
umaal lr, r4, r1, r3 @ {lr, r4} = ((U64)r1 * (U64)r3) + lr + r4
adds r0, lr, r12 @ <-.
@ {r0, r1} = (U64){lr, r4} + (U64){r12, r5}
adc r1, r4, r5 @ <-'
Ich glaube nicht, dass ich es mehr als das optimieren kann. Das sind nur 8 Anweisungen.
Ich habe XXH_mult32to64
hinzugefügt, das auf MSVC zu __emulu
erweitert wird, wodurch es viel weniger wahrscheinlich ist, dass MSVC einen __allmul
Aufruf ausgibt (obwohl dies fairerweise nicht der Fall ist) es in diesem Fall).
Ich denke immer noch, dass x86 weiter optimiert werden kann, obwohl es möglicherweise nur die festen Ausgaberegister sind, die all das Mischen verursachen. Es läuft eigentlich recht schnell:
uint32_t val32[4];
uint64_t *val = val32;
uint64_t sum = 0;
srand(0);
double start = (double)clock();
for (int i = 0; i < 10000000; i++) {
val32[0] = rand();
val32[1] = rand();
val32[2] = rand();
val32[3] = rand();
sum += XXH_mul128AndFold_32(val[0], val[1]);
}
double end = (double)clock();
printf("%lld %lfs\n", sum, (end - start) / CLOCKS_PER_SEC);
```
7652620537862933594 0.454625s
For comparison, this is in 64-bit mode with `__uint128_t`:
7652620537862933594 0.336406
Only 35% slower considering how much more work it does.
Side note: Clang optimizes the `__uint128_t` version to this on aarch64, so that is probably best to use it. Fused multiply and add is always preferred.
```asm
umulh x8, x1, x0 // x8 = ((__uint128_t)x1 * x0) >> 64
madd x0, x1, x0, x8 // x0 = (x1 * x0) + x8;
Sieht großartig aus !
Auf ARM ist es trotz der deutlich weniger Anweisungen definitiv langsamer, aber das liegt hauptsächlich daran, dass der rand()-Aufruf auf Bionic ziemlich ausgefeilt ist.
967456348838854209 5.572055s
Ersetzen Sie es durch nicht initialisierte Daten, zwingen Sie es in ein Register und verwenden Sie -fno-inline
, um zu verhindern, dass es betrügt, und es ist sehr schnell:
5764888493227865371 0.119198s
(Ohne die rand
Aufrufe für x86 ist es 0.076690s
meine Implementierung vs. 0.031167
natives x86_64, was mehr Sinn macht. Trotzdem ziemlich schnell.)