x86 & gcc ... így szeretjük

Az alábbi C kódot gcc -O2 .... Lefordítva, lefuttatva az eredmény:
     Szegmentálási hiba (core készült)

#include <smmintrin.h>
float a[1000];

int main() {
    __v4sf res = {0, 0, 0, 0};
    for (int i=0; i<100; i+=4) {
        __v4sf data1 = *((__v4sf *)&a[i+5]); // 4: OK, 5: Segfault
        __v4sf data2 = *((__v4sf *)&a[i+6]);
        __v4sf data3 = *((__v4sf *)&a[i+13]);
        res += data1 * data2 * data3;
    }
    return res[0] + res[1] + res[2] + res[3];
}

Átírod i+4-re a fenti értéket, gcc -O2 és frankón fut.
ARM processzorokon az eredetileg megírt bonyolultabb kódban sem tudtam olyan esetet előidézni, hogy a float32x4 műveletek segfault-oljanak.

Hogyan lehet rábeszélni a C fordítót, hogy a fenti kód i+5 esetén is használható legyen x86-on is?
Vajon Intel specifikus a hiba, vagy a Ryzen is segfault-ot dob?

Hozzászólások

alignolasssal lesz valami (4, 8, 12-vel jo).

amugy meg O2-vel vigyazni, mert az ilyen konstans ertekeket elore kiszamitja, es nincs is loopod meg res-ben szummazasod!

A vegtelen ciklus is vegeter egyszer, csak kelloen eros hardver kell hozza!

A GCC rossz kódot fordít, rossz utasítást használ és alignment crasht produkál. Ha megnézed az ASM forrást

gcc kecske.c -O2 -S -o kecske.s
cat kecske.s
	.file	"faszom.c"
	.text
	.section	.text.startup,"ax",@progbits
	.p2align 4,,15
	.globl	main
	.type	main, @function
main:
.LFB641:
	.cfi_startproc
	leaq	20+a(%rip), %rax
	pxor	%xmm1, %xmm1
	leaq	400(%rax), %rdx
	.p2align 4,,10
	.p2align 3
.L2:
	movaps	(%rax), %xmm0
	addq	$16, %rax
	mulps	-12(%rax), %xmm0
	mulps	16(%rax), %xmm0
	cmpq	%rax, %rdx
	addps	%xmm0, %xmm1
	jne	.L2
	movaps	%xmm1, %xmm2
	movaps	%xmm1, %xmm0
	shufps	$85, %xmm1, %xmm2
	addss	%xmm2, %xmm0
	movaps	%xmm1, %xmm2
	unpckhps	%xmm1, %xmm2
	shufps	$255, %xmm1, %xmm1
	addss	%xmm2, %xmm0
	addss	%xmm0, %xmm1
	cvttss2si	%xmm1, %eax
	ret
	.cfi_endproc
.LFE641:
	.size	main, .-main
	.comm	a,4000,32
	.ident	"GCC: (Debian 7.3.0-1~mx17+1) 7.3.0"
	.section	.note.GNU-stack,"",@progbits

akkor GDB-vel végigsteppelve kiderül, hogy az érintett utasítás a "movaps (%rax), %xmm0". A MOVAPS pedig megköveteli, hogy ha memóriahozzáférés is van, akkor az 16-tal osztható címen történjen. Márpedig te egy float tömböt indexelsz, ami 32-bites értékekből áll, azaz csak a 4-gyel osztható indexek lesznek 128 bitre-ra igazítva.

A CLang azért működik, mert ha megnézed a forrást

clang kecske.c -O2 -S -o kecske.s
cat kecske.s
	.text
	.file	"faszom.c"
	.globl	main                    # -- Begin function main
	.p2align	4, 0x90
	.type	main,@function
main:                                   # @main
	.cfi_startproc
# %bb.0:
	xorps	%xmm1, %xmm1
	xorl	%eax, %eax
	jmp	.LBB0_1
	.p2align	4, 0x90
.LBB0_2:                                #   in Loop: Header=BB0_1 Depth=1
	movups	a+36(,%rax,4), %xmm1
	movups	a+40(,%rax,4), %xmm2
	mulps	%xmm1, %xmm2
	movups	a+68(,%rax,4), %xmm1
	mulps	%xmm2, %xmm1
	addps	%xmm1, %xmm0
	addq	$8, %rax
	movaps	%xmm0, %xmm1
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
	movups	a+20(,%rax,4), %xmm0
	movups	a+24(,%rax,4), %xmm2
	mulps	%xmm0, %xmm2
	movups	a+52(,%rax,4), %xmm0
	mulps	%xmm2, %xmm0
	addps	%xmm1, %xmm0
	leaq	4(%rax), %rcx
	cmpq	$99, %rcx
	jbe	.LBB0_2
# %bb.3:
	movaps	%xmm0, %xmm1
	shufps	$229, %xmm0, %xmm1      # xmm1 = xmm1[1,1],xmm0[2,3]
	addss	%xmm0, %xmm1
	movaps	%xmm0, %xmm2
	movhlps	%xmm0, %xmm2            # xmm2 = xmm0[1],xmm2[1]
	addss	%xmm1, %xmm2
	shufps	$231, %xmm0, %xmm0      # xmm0 = xmm0[3,1,2,3]
	addss	%xmm2, %xmm0
	cvttss2si	%xmm0, %eax
	retq
.Lfunc_end0:
	.size	main, .Lfunc_end0-main
	.cfi_endproc
                                        # -- End function
	.type	a,@object               # @a
	.comm	a,4000,16

	.ident	"clang version 7.1.0-svn353565-1~exp1~20190407125230.69 (branches/release_70)"
	.section	".note.GNU-stack","",@progbits
	.addrsig

akkor látod, hogy az nem MOVAPS-ot, hanem MOVUPS-ot használ a memória piszkálásakor, aminek nem szükséges a 16 byte-os alignment.

SSE extension-t használsz, és nem 16 bájtra nem illeszkedő címet kasztolsz __v4sf-ra, ami illegál. Szerintem nem fordító hiba ez. A hibajelzés mondhatna többet, de ez van.

Compiler explorerben ezek a rovid kodok egyszeruen megnezhetoek tobb forditoval: https://godbolt.org/

Az alignment crashek elkerulesere sosem szegyen memcpy-t hasznalni, szinte minden ilyen egyszeru esetben egy sima ertekadasra fog fordulni, meg -O0 esetben is.

#include <smmintrin.h>
#include <string.h>
float a[1000];

int main() {
    __v4sf res = {0, 0, 0, 0};
    for (int i=0; i<100; i+=4) {
        __v4sf data1, data2, data3;
        memcpy(&data1, a + i + 5, sizeof(__v4sf));
        memcpy(&data2, a + i + 6, sizeof(__v4sf));
        memcpy(&data3, a + i + 13, sizeof(__v4sf));
        res += data1 * data2 * data3;
    }
    return res[0] + res[1] + res[2] + res[3];
}

mindegyik memcpy-bol egy MOVUPS lesz.

Ügyes megkerülő megoldás tetszik!
Nagysebességű real konvoluciót optimalizálok, kipróbáltam különböző fránya esetekre. Utólag clang-gal is lefordítottam, melléírtam az időket.

x86 laptopon:
# _mm_loadu_ps(&input->data[i+j+1]) módszer SSE-re
# _mm256_loadu_ps(&input->data[i+j+1]) módszer AVX-hoz
./benchmark
test1 --> eltime: 167.2 ms, result: 22803.118912  clang: 148,8 ms ... nyers algoritmus tempó, csak GCC maszírozás
test2 --> eltime: 268.7 ms, result: 22803.188731  clang: 290,7 ms ... simán NEON/__v4sf regiszterrel (4 float-os SIMD)
test3 --> eltime: 113.0 ms, result: 22803.188731  clang: 159,8 ms ... 4-es csoport, coeff load kimélés
test4 --> eltime: 113.0 ms, result: 22803.188700  clang: 139,1 ms ... 8-as csoport
test5 --> eltime: 149.4 ms, result: 22803.188700  clang: 136,8 ms ... 16-os csoport
t4AVX --> eltime: 100.8 ms, result: 22803.135737 clang: 106,3 ms ... 16-os csoport 8 széles AVX-es feldolgozással

# memcpy ... SSE-nél oké, AVX-re gcc esetén a makrózott intrinsic győzött
./benchmark-memcpy
test1 --> eltime: 168.8 ms, result: 22803.118912  clang: 145,8 ms ... nyers algoritmus tempó, csak GCC maszírozás
test2 --> eltime: 268.8 ms, result: 22803.188731  clang: 291,1 ms
test3 --> eltime: 111.3 ms, result: 22803.188731  clang: 159,4 ms
test4 --> eltime: 112.2 ms, result: 22803.188700  clang: 138,8 ms
test5 --> eltime: 154.6 ms, result: 22803.188700  clang: 135,0 ms
t4AVX --> eltime: 295.0 ms, result: 22803.135737 clang: 121,4 ms

AArch64 (Odroid-C2):
# float32x4_t data = *((float32x4_t *)&input->data[i+j+1]) kivétel nélkül
./benchmark
test1 --> eltime: 1173.5 ms, result: 22803.267825  clang: 1197 ms ... nyers algoritmus tempó, csak GCC maszírozás
test2 --> eltime: 1172.7 ms, result: 22803.304331  clang: 1490 ms
test3 --> eltime: 881.4 ms, result: 22803.304331    clang: 1296 ms
test4 --> eltime: 533.0 ms, result: 22803.304331    clang: 973 ms
test5 --> eltime: 517.4 ms, result: 22803.304331    clang: 865 ms

# memcpy ... oké
./benchmark-memcpy
test1 --> eltime: 1173.3 ms, result: 22803.267825  clang: 1197 ms ... nyers algoritmus tempó, csak GCC maszírozás
test2 --> eltime: 1279.4 ms, result: 22803.304331  clang: 1492 ms
test3 --> eltime: 892.2 ms, result: 22803.304331    clang: 1296 ms
test4 --> eltime: 548.8 ms, result: 22803.304331    clang:  974 ms
test5 --> eltime: 543.6 ms, result: 22803.304331    clang:  865 ms

Tetszik a memcpy() ilyen  felhasználása. Nem gondoltam, hogy ennyire jól beilleszti és kioptimalizálja a C fordító ezt a libc-s megoldást.