Zwróciłem się do współpracownika, if (i < input.size() - 1) print(0);
który zoptymalizowałby się w tej pętli, aby input.size()
nie był czytany przy każdej iteracji, ale okazuje się, że tak nie jest!
void print(int x) {
std::cout << x << std::endl;
}
void print_list(const std::vector<int>& input) {
int i = 0;
for (size_t i = 0; i < input.size(); i++) {
print(input[i]);
if (i < input.size() - 1) print(0);
}
}
Według Compiler Explorera z opcjami gcc -O3 -fno-exceptions
czytamy input.size()
każdą iterację i używamy lea
do odejmowania!
movq 0(%rbp), %rdx
movq 8(%rbp), %rax
subq %rdx, %rax
sarq $2, %rax
leaq -1(%rax), %rcx
cmpq %rbx, %rcx
ja .L35
addq $1, %rbx
Co ciekawe, w Rust występuje taka optymalizacja. Wygląda na to, że i
zostaje zastąpiony zmienną, j
która jest zmniejszana przy każdej iteracji, a test i < input.size() - 1
jest zastępowany przez coś podobnego j > 0
.
fn print(x: i32) {
println!("{}", x);
}
pub fn print_list(xs: &Vec<i32>) {
for (i, x) in xs.iter().enumerate() {
print(*x);
if i < xs.len() - 1 {
print(0);
}
}
}
W Eksploratorze kompilatorów odpowiedni zestaw wygląda następująco:
cmpq %r12, %rbx
jae .LBB0_4
Sprawdziłem i jestem prawie pewien, że r12
jest xs.len() - 1
i rbx
jest licznikiem. Wcześniej istnieje pętla add
for rbx
i mov
zewnętrzna dla pętli r12
.
Dlaczego to? Wygląda na to, że jeśli GCC jest w stanie wprowadzić size()
i operator[]
tak jak miało to miejsce, powinien wiedzieć, że size()
to się nie zmienia. Ale może optymalizator GCC ocenia, że nie warto wyciągać go do zmiennej? A może jest jakiś inny możliwy efekt uboczny, który uczyniłby to niebezpiecznym - czy ktoś wie?
println
Jest to również prawdopodobnie złożona metoda, kompilator może mieć problemy z udowodnieniem, żeprintln
nie mutuje wektora.cout.operator<<()
. Kompilator nie wie, że ta funkcja czarnej skrzynki nie otrzymuje odwołania dostd::vector
globalnego.println
luboperator<<
klucz jest kluczowa.Odpowiedzi:
Nieliniowe wywołanie funkcji do
cout.operator<<(int)
jest czarną skrzynką dla optymalizatora (ponieważ biblioteka jest właśnie napisana w C ++ i wszystko, co widzi optymalizator, jest prototypem; patrz dyskusja w komentarzach). Musi zakładać, że każda pamięć, na którą może wskazywać zmienna globalna, została zmodyfikowana.(Lub
std::endl
połączenie. BTW, po co wymuszać kolor w tym momencie zamiast po prostu drukować'\n'
?)np . jak wiadomo,
std::vector<int> &input
jest odwołaniem do zmiennej globalnej, a jedno z tych wywołań funkcji modyfikuje zmienną globalną . (Albo jestvector<int> *ptr
gdzieś globalny , albo jest funkcja, która zwraca wskaźnik dostatic vector<int>
jakiegoś innego urządzenia kompilacyjnego, lub w inny sposób, że funkcja może uzyskać odwołanie do tego wektora bez przekazywania nam przez niego odwołania.Jeśli masz zmienną lokalną, której adres nigdy nie został pobrany, kompilator może założyć, że wywołania funkcji innych niż wbudowane nie mogą jej mutować. Ponieważ żadna zmienna globalna nie byłaby w stanie utrzymać wskaźnika do tego obiektu. ( To się nazywa Analiza ucieczki ). Dlatego kompilator może przechowywać
size_t i
rejestr w wywołaniach funkcji. (int i
można go po prostu zoptymalizować, ponieważ jest zasłoniętysize_t i
i nie jest używany w inny sposób).To samo może zrobić z lokalnymi
vector
(tj. Dla wskaźników base, end_size i end_capacity).ISO C99 ma rozwiązanie tego problemu:
int *restrict foo
. Wiele kompilacji C ++ obsługujeint *__restrict foo
obietnicę, że pamięć wskazywana przezfoo
jest dostępna tylko przez ten wskaźnik. Najczęściej przydatny w funkcjach, które zajmują 2 tablice i chcesz obiecać kompilatorowi, że się nie nakładają. Może więc automatycznie wektoryzować bez generowania kodu, aby to sprawdzić i uruchomić pętlę rezerwową.Komentarze PO:
To wyjaśnia, dlaczego Rust może dokonać takiej optymalizacji, a C ++ nie.
Optymalizacja C ++
Oczywiście powinieneś użyć
auto size = input.size();
raz na górze swojej funkcji, aby kompilator wiedział, że jest to niezmienna pętla. Implementacje C ++ nie rozwiązują tego problemu, więc musisz to zrobić sam.Może być również konieczne
const int *data = input.data();
podniesienie obciążenia wskaźnika danych zstd::vector<int>
„bloku kontrolnego”. To niefortunne, że optymalizacja może wymagać bardzo nieidiomatycznych zmian źródła.Rust jest znacznie bardziej nowoczesnym językiem, zaprojektowanym po tym, jak twórcy kompilatorów dowiedzieli się, co było w praktyce możliwe dla kompilatorów. Naprawdę pokazuje to również na inne sposoby, w tym przenośnie odsłaniając niektóre fajne rzeczy, które procesory mogą zrobić poprzez
i32.count_ones
, obracanie, skanowanie bitów itp. To naprawdę głupie, że ISO C ++ nadal nie ujawnia żadnego z nich przenośnie, z wyjątkiem tegostd::bitset::count()
.źródło
operator<<
tych typów operandów; więc w Standard C ++ nie jest to czarna skrzynka i kompilator może założyć, że robi to, co mówi dokumentacja. Może chcą wesprzeć twórców bibliotek dodając niestandardowe zachowanie ...cout
pozwala nastreambuf
powiązanie obiektu klasy zdefiniowanej przez użytkownika ze strumieniem za pomocącout.rdbuf
. Podobnie obiektostream
powiązany z może być powiązanycout.tie
.this
wskaźnik został niejawnie przekazany. Może się to zdarzyć w praktyce już od konstruktora. Rozważ tę prostą pętlę - sprawdziłem tylko główną pętlę gcc (odL34:
dojne L34
), ale zdecydowanie zachowuje się, jakby elementy wektorowe uciekły (ładując je z pamięci przy każdej iteracji).