Naiwnie napisany mandelbrot w Asm wychodzi lepiej od GCC

4

Ćwiczyłem sobie ostatnio System V AMD64 ABI w asmie - otak, żeby zobaczyć dlaczego jvm wypluwa taki kod jak wypluwa.
Do tego jeszcze pobieżnie spojrzałem na SSE i AVX.
Odpadem z ćwiczeń jest rysowanie mandlebrota pod xlib - gdzie algorytm zrobilem w asm.
Kod:
https://github.com/jarekratajski/yamandel

Dziwnie wyszło. Asm szybszy prawie 2 razy od gcc.
Może dlatego, że:

  • nie jestem biegły w C (20 lat nie pisze - co najwyżej oglądam i poprawiam bugi, a i to raz na ruski rok).
  • w assemblerze też podobnie - w zasadzie to znałem tylko 6502 i 68k, a w tych nie byłem biegły (a x86 był dla mnie totalnie obrzydliwy).

Jakim cudem, że moj bieda asm jest szybszy od tego co gcc produkuje ? (gcc miał tak ładnie wektoryzować) To jest tym bardziej dziwne, że ten asm powstawał na zasadzie rozpaczliwej, czasem powtarzam te same operacje bo się pogubiłem co w którym rejestrze jest - mistrzostwo świata to to na pewno nie jest :-).

Pytanie - co zepsułem?

  • coś uprościłem w asm ?
  • zrypałem parametry gcc?
  • zrypałem kod w c?

To śmieszne, np. w kontekście wątku : https://4programmers.net/Forum/Inzynieria_oprogramowania/328037-jak_duze_spowolnienie_wynika_uzywania_jezykow_wysokiego_poziomu
Może c nie jest wysokiego poziomu, ale gcc coś powinien umieć.

Za jakiś czas sprawdze jak to robi jvm...

1

Spojrzałem na Makefile i może oprócz zestawu instrukcji dla danej architektury (-march=native), to trzeba jeszcze optymalizacja pod daną architekturę włączyć?

Using -march=native enables all instruction subsets supported by the local machine (hence the result might not run on different machines).
Using -mtune=native produces code optimized for the local machine under the constraints of the selected instruction set.
2

Patrząc po makefile, kompilujesz bez optymalizacji. (CFLAGS nie jest użyte).

time of asm calculations:140450
time of c calculations:82850
time of asm calculations:218151
time of c calculations:80168
time of asm calculations:242156
time of c calculations:99624
time of asm calculations:348954
time of c calculations:92590
time of asm calculations:280319
time of c calculations:88019
time of asm calculations:252104
time of c calculations:35209

Tak mi to wygląda po dodaniu CFLAGS.

Swoją drogą, clock() to słaba funkcja do benchmarku tutaj - ona zlicza czas, ale operacje są liczone ze wszystkich wątków jednocześnie (więc w ciągu sekundy może naliczyć 16 sekund na 16 core'ach).

Swoją drogą #2, po włączeniu optymalizacji obraz się psuje, więc stawiam na błąd w kodzie: https://i.imgur.com/4jsfjbs.png

0

Normalnie liczę na jednym wątku (ustawiam zmienną, i dziwne, że CFLAGS nie jest używe bo mi się wyświetla w konsoli i widze różnice w kodzie (ustawiałem różne wersje).

W branchu https://github.com/jarekratajski/yamandel/tree/tuning
ustawiłem mierzenie czasu + dodałem oble mtune.
Nie widze róznicy.
Prosty test, jesli zwalę opcje CFLAGS na coś dziwnego pokazuje, że jednak są wykorzystywane (bo kompilacja nie działa).
Z drugiej strony miałem dziwny problem z tym, że -DCOMPARE_TO_C jakoś nie jest do C przekazwyane, więc coś może być na rzeczy.

3
yamandel: yamandel.o yamandel.c yasmandel.o
	$(CC) yamandel.c -L/usr/X11R6/lib  -lpthread -lX11 yasmandel.o -o yamandel

Tutaj nie używasz, powinno być:

yamandel: yamandel.o yamandel.c yasmandel.o
	$(CC) $(CFLAGS) yamandel.c -L/usr/X11R6/lib  -lpthread -lX11 yasmandel.o -o yamandel

Nie analizowałem całości kodu, ale w tle nie latają inne wątki? Tak czy inaczej, użyłbym C++ <chrono> do mierzenia czasu, albo jakichś posixowych funkcji, a nie clock().

1

To było to. Teraz jest dobrze i C jest szybsze ! Teraz poanalizuje co produkuje gcc i będęm się jarał :-)

EDIT: te artefakty (bug) się pojawiały jak jednoczesnie na wielu wątkach było liczenie i w c i w asm -> po tej samej tablicy.
Chociaż imo... nie powinny, ale pewnie czegoś nie wiem... co może być istotne. (Znalazłem - zrobiłem optymalizacje w asm - nie wyszła - czegoś się muszę douczyć).

W kodzie produkowanym przez gcc pojawiło się kilka instrukcji.....DD (czyli faktycznie na wektorach).
-mtune=native chyba nic nie zmienia (nie w czasach i nie w krytycznym kodzie).
EDIT2: po bardziej kryrycznym przeglądzie - kod jest szybszy, ale jednak użycie wektorów nadal słabe (chyba się skusze na wyprzedzenie tego w asm).

1
  1. Wymiatacze robią to w C z intrinsics.
    Nie jest to piękne, ale to chyba najszybsza opcja.
    Tylko że to praktycznie programowanie w ASM.

Przykład #1 (niekoniecznie najszybszy): https://nullprogram.com/blog/2015/07/10/
Przykład #2: https://benchmarksgame-team.pages.debian.net/benchmarksgame/program/mandelbrot-gcc-6.html

  1. Idealnie byłoby zrobić to z "omp simd", tylko wymaga to zmiany sensu pętli (trzeba robić pętlę na całym wektorze a nie na pikselu), przykład:
    https://martin-ueding.de/articles/mandelbrot-performance/index.html
2

@jarekr000000: Jeśli nadal chcesz zaklepać wersję w Javie to spróbuj z Project Panama z brancha vectorIntrinsics (trzeba sobie zbudować JDK ze źródeł): http://hg.openjdk.java.net/panama/dev/branches
W środku ma klasy reprezentujące wektory SIMD, np: http://hg.openjdk.java.net/panama/dev/file/vectorIntrinsics/src/jdk.incubator.vector/share/classes/jdk/incubator/vector

3
yamandel: yamandel.o yamandel.c yasmandel.o
	$(CC) $(CFLAGS) yamandel.c -L/usr/X11R6/lib  -lpthread -lX11 yasmandel.o -o yamandel

ja bym się jeszcze przyczepił że w jednej komendzie mieszana jest kompilacja C z linkowaniem, częściowo z wykorzystaniem gotowych plików .o.

Raczej w makefile kompilację robi się osobno (czy to C czy asm), a potem osobnym wywołaniem linkuje wszystko razem - przy okazji można zdefinować flagi dla linkera w LFLAGS:

CFLAGS = ...
LFLAGS = -L/usr/X11R6/lib  -lpthread -lX11

yamandel: yamandel.o yasmandel.o
	$(CC) $(LFLAGS) $^ -o $@

yamandel.o: yamandel.c
	$(CC) -c $(CFLAGS) $<

yasmandel.o: yasmandel.asm
	...

przy większej liczbie plików .c można to dalej omakrować żeby nie powtarzać reguł.

1 użytkowników online, w tym zalogowanych: 0, gości: 1