Tak więc miałem ten kod:
constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
for(int i = 0; i < N; ++i) {
sum[i] = a[i] + b[i];
}
}
void f2(char* sum, char* a, char* b) {
char* end = sum + N;
while(sum != end) {
*sum++ = *a++ + *b++;
}
}
Chciałem zobaczyć kod, który wygeneruje GCC 4.7.2. Więc pobiegłem g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11
i otrzymałem następujący wynik:
.file "a.c++"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl _Z2f1PcS_S_
.type _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
.cfi_startproc
lea rcx, [rdx+16]
lea rax, [rdi+16]
cmp rdi, rcx
setae r8b
cmp rdx, rax
setae cl
or cl, r8b
je .L5
lea rcx, [rsi+16]
cmp rdi, rcx
setae cl
cmp rsi, rax
setae al
or cl, al
je .L5
xor eax, eax
.p2align 4,,10
.p2align 3
.L3:
movdqu xmm0, XMMWORD PTR [rdx+rax]
movdqu xmm1, XMMWORD PTR [rsi+rax]
paddb xmm0, xmm1
movdqu XMMWORD PTR [rdi+rax], xmm0
add rax, 16
cmp rax, 992
jne .L3
mov ax, 8
mov r9d, 992
.L2:
sub eax, 1
lea rcx, [rdx+r9]
add rdi, r9
lea r8, [rax+1]
add rsi, r9
xor eax, eax
.p2align 4,,10
.p2align 3
.L4:
movzx edx, BYTE PTR [rcx+rax]
add dl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], dl
add rax, 1
cmp rax, r8
jne .L4
rep
ret
.L5:
mov eax, 1000
xor r9d, r9d
jmp .L2
.cfi_endproc
.LFE0:
.size _Z2f1PcS_S_, .-_Z2f1PcS_S_
.p2align 4,,15
.globl _Z2f2PcS_S_
.type _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
.cfi_startproc
lea rcx, [rdx+16]
lea rax, [rdi+16]
cmp rdi, rcx
setae r8b
cmp rdx, rax
setae cl
or cl, r8b
je .L19
lea rcx, [rsi+16]
cmp rdi, rcx
setae cl
cmp rsi, rax
setae al
or cl, al
je .L19
xor eax, eax
.p2align 4,,10
.p2align 3
.L17:
movdqu xmm0, XMMWORD PTR [rdx+rax]
movdqu xmm1, XMMWORD PTR [rsi+rax]
paddb xmm0, xmm1
movdqu XMMWORD PTR [rdi+rax], xmm0
add rax, 16
cmp rax, 992
jne .L17
add rdi, 992
add rsi, 992
add rdx, 992
mov r8d, 8
.L16:
xor eax, eax
.p2align 4,,10
.p2align 3
.L18:
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, r8
jne .L18
rep
ret
.L19:
mov r8d, 1000
jmp .L16
.cfi_endproc
.LFE1:
.size _Z2f2PcS_S_, .-_Z2f2PcS_S_
.ident "GCC: (GNU) 4.7.2"
.section .note.GNU-stack,"",@progbits
Ssam w czytaniu, więc postanowiłem dodać kilka markerów, aby wiedzieć, gdzie poszły korpusy pętli:
constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
for(int i = 0; i < N; ++i) {
asm("# im in ur loop");
sum[i] = a[i] + b[i];
}
}
void f2(char* sum, char* a, char* b) {
char* end = sum + N;
while(sum != end) {
asm("# im in ur loop");
*sum++ = *a++ + *b++;
}
}
I GCC wypluł to:
.file "a.c++"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl _Z2f1PcS_S_
.type _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
.cfi_startproc
xor eax, eax
.p2align 4,,10
.p2align 3
.L2:
#APP
# 4 "a.c++" 1
# im in ur loop
# 0 "" 2
#NO_APP
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, 1000
jne .L2
rep
ret
.cfi_endproc
.LFE0:
.size _Z2f1PcS_S_, .-_Z2f1PcS_S_
.p2align 4,,15
.globl _Z2f2PcS_S_
.type _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
.cfi_startproc
xor eax, eax
.p2align 4,,10
.p2align 3
.L6:
#APP
# 12 "a.c++" 1
# im in ur loop
# 0 "" 2
#NO_APP
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, 1000
jne .L6
rep
ret
.cfi_endproc
.LFE1:
.size _Z2f2PcS_S_, .-_Z2f2PcS_S_
.ident "GCC: (GNU) 4.7.2"
.section .note.GNU-stack,"",@progbits
Jest to znacznie krótsze i ma kilka istotnych różnic, takich jak brak instrukcji SIMD. Spodziewałem się tego samego wyniku, z kilkoma komentarzami gdzieś pośrodku. Czy robię tu jakieś błędne założenie? Czy optymalizator GCC jest utrudniony przez komentarze ASM?
c++
gcc
assembly
optimization
inline-assembly
R. Martinho Fernandes
źródło
źródło
asm
formularz z pustymi listami wyników i clobberów.asm("# im in ur loop" : : );
(patrz dokumentacja )-fverbose-asm
flagę, która dodaje kilka adnotacji, aby pomóc zidentyfikować, jak rzeczy poruszają się między rejestrami.Odpowiedzi:
Interakcje z optymalizacjami są wyjaśnione mniej więcej w połowie strony „Instrukcje asemblera z operandami wyrażenia C” w dokumentacji.
GCC nie próbuje zrozumieć żadnego aktualnego zestawu wewnątrz
asm
; jedyne, co wie o zawartości, to to, co (opcjonalnie) podasz jej w specyfikacji argumentu wyjściowego i wejściowego oraz na liście blokowania rejestru.W szczególności zwróć uwagę:
i
Zatem obecność
asm
wewnątrz pętli zahamowała optymalizację wektoryzacji, ponieważ GCC zakłada, że ma to skutki uboczne.źródło
asm
instrukcja musi być uruchamiana raz na zawsze, gdy byłaby wykonywana na abstrakcyjnej maszynie C ++, a GCC decyduje się nie wektoryzować, a następnie emituje asm 16 razy z rzędu napaddb
. Myślę, że to jest legalne, ponieważ dostęp do postaci nie jestvolatile
. (W przeciwieństwie do rozszerzonego oświadczenia asm z"memory"
uderzeniem)Zauważ, że gcc zwektoryzował kod, dzieląc ciało pętli na dwie części, przy czym pierwsza przetwarza po 16 elementów, a druga wykonuje resztę później.
Jak skomentował Ira, kompilator nie parsuje bloku asm, więc nie wie, że to tylko komentarz. Nawet gdyby tak było, nie ma możliwości dowiedzenia się, co zamierzałeś. Zoptymalizowane pętle mają podwojony korpus, czy powinien umieścić twój asm w każdej? Czy chciałbyś, aby nie był wykonywany 1000 razy? Nie wie, więc idzie bezpieczną trasą i wraca do prostej pojedynczej pętli.
źródło
Nie zgadzam się z „gcc nie rozumie, co jest w
asm()
bloku”. Na przykład gcc radzi sobie całkiem dobrze z optymalizacją parametrów, a nawet z ponownym rozmieszczeniemasm()
bloków tak, aby przeplatały się z wygenerowanym kodem C. Dlatego, jeśli spojrzysz na asembler wbudowany, na przykład w jądrze Linuksa, prawie zawsze jest on poprzedzony przedrostkiem,__volatile__
aby zapewnić, że kompilator „nie przenosi kodu”. Kazałem gcc przesunąć moje "rdtsc", co pozwoliło mi zmierzyć czas potrzebny do zrobienia określonej rzeczy.Jak zostało udokumentowane, gcc traktuje pewne typy
asm()
bloków jako „specjalne” i dlatego nie optymalizuje kodu po żadnej ze stron bloku.Nie oznacza to, że gcc czasami nie będzie zdezorientowany przez wbudowane bloki asemblera lub po prostu zdecyduje się zrezygnować z jakiejś szczególnej optymalizacji, ponieważ nie może podążać za konsekwencjami kodu asemblera itp. często można się pomylić z powodu braku tagów clobber - więc jeśli masz jakieś instrukcje, takie jak
cpuid
to zmienia wartość EAX-EDX, ale napisałeś kod tak, że używa tylko EAX, kompilator może przechowywać rzeczy w EBX, ECX i EDX, a wtedy twój kod działa bardzo dziwnie, gdy te rejestry są nadpisywane ... Jeśli masz szczęście, zawiesza się natychmiast - wtedy łatwo zorientować się, co się dzieje. Ale jeśli masz pecha, to się zawiesza ... Kolejną trudną rzeczą jest instrukcja dzielenia, która daje drugi wynik w edx. Jeśli nie przejmujesz się modulo, łatwo zapomnieć, że EDX został zmieniony.źródło
volatile asm
informuje GCC, że kod może mieć „ważne skutki uboczne” i zajmie się nim ze szczególną uwagą. Może nadal zostać usunięty w ramach optymalizacji martwego kodu lub przeniesiony. Interakcja z kodem C musi zakładać taki (rzadki) przypadek i narzucać ścisłą sekwencyjną ocenę (np. Poprzez tworzenie zależności w asm).asm("")
) jest niejawnie niestabilny, tak jak rozszerzony asm bez argumentów wyjściowych. GCC nie rozumie łańcucha szablonu asm, tylko ograniczenia; Dlatego jest to niezbędne , aby dokładnie i całkowicie opisać swój asm do kompilatora stosując ograniczeń. Podstawianie operandów do ciągu szablonu nie wymaga więcej zrozumienia niżprintf
użycie ciągu formatu. TL: DR: nie używaj GNU C Basic asm do niczego, może z wyjątkiem przypadków użycia takich jak ten z czystymi komentarzami.Ta odpowiedź jest teraz zmodyfikowana: pierwotnie została napisana z myślą o rozważaniu inline Basic Asm jako dość silnie sprecyzowanego narzędzia, ale w GCC to nic takiego. Podstawowy Asm jest słaby, więc odpowiedź została zredagowana.
Każdy komentarz zespołu działa jako punkt przerwania.
EDYCJA: Ale zepsuty, ponieważ używasz Basic Asm. Inline
asm
(asm
instrukcja wewnątrz ciała funkcji) bez jawnej listy clobber jest słabo określoną funkcją w GCC i jej zachowanie jest trudne do zdefiniowania. Nie wydaje się (nie rozumiem w pełni jego gwarancji) związanego z czymkolwiek konkretnym, więc chociaż kod asemblera musi zostać uruchomiony w pewnym momencie, jeśli funkcja jest uruchomiona, nie jest jasne, kiedy jest uruchamiana dla dowolnego trywialny poziom optymalizacji . Punkt przerwania, który można zmienić za pomocą sąsiedniej instrukcji, nie jest zbyt użytecznym „punktem przerwania”. KONIEC EDYCJIMożesz uruchomić swój program w interpretatorze, który przerywa każdy komentarz i wypisuje stan każdej zmiennej (używając informacji debugowania). Punkty te muszą istnieć, abyś obserwował otoczenie (stan rejestrów i pamięci).
Bez komentarza nie istnieje żaden punkt obserwacyjny, a pętla jest kompilowana jako pojedyncza funkcja matematyczna przyjmująca środowisko i tworząca zmodyfikowane środowisko.
Chcesz poznać odpowiedź na bezsensowne pytanie: chcesz wiedzieć, jak jest kompilowana każda instrukcja (lub może blok, a może zakres instrukcji), ale żadna pojedyncza izolowana instrukcja (lub blok) nie jest kompilowana; całość jest kompilowana jako całość.
Lepszym pytaniem byłoby:
Witam GCC. Dlaczego uważasz, że to wyjście asm implementuje kod źródłowy? Prosimy o wyjaśnienie krok po kroku, przy każdym założeniu.
Ale wtedy nie chciałbyś czytać dowodu dłuższego niż wyjście asm, napisane w ramach wewnętrznej reprezentacji GCC.
źródło
"memory"
clobbera) jako bandaid dla istniejącego błędnego kodu, który z pewnością istnieje. Nawet w przypadku takich instrukcji,asm("cli")
które wpływają tylko na część stanu architektury, której kod wygenerowany przez kompilator nie dotyka, nadal potrzebujesz uporządkowanego wrt. ładunki / magazyny generowane przez kompilator (np. jeśli wyłączasz przerwania wokół krytycznej sekcji).add rsp, -128
najpierw. Ale robienie tego jest po prostu oczywistym zamroczeniem.asm("" :::)
(niejawnie niestabilny, ponieważ nie ma danych wyjściowych, ale nie jest powiązany z resztą kodu przez zależności wejścia lub wyjścia"memory"
. I oczywiście nie dokonuje%operand
zamiany w łańcuchu szablonu, więc%
nie trzeba uciekać jako%%
. Tak więc, zgodziliśmy się, wycofanie Basic Asm poza__attribute__((naked))
funkcje i zakres globalny byłoby dobrym pomysłem.