Docelowi odbiorcy
  • Potencjalni i średnio-zaawansowani deweloperzy Rust, którzy martwią się wolnymi czasami kompilacji, ich skalą i praktykami zapobiegawczymi.
  • Zaawansowani deweloperzy Rust, którzy pomogą mi poprawić czas kompilacji.
Pochodzenie Moje doświadczenie w Rust.
Nastrój Trochę udany; trochę rozczarowany.
Język Obcy. Moim pierwszym język jest angielki. Jeśli zobaczysz błąd, niezależnie od tego, czy chodzi o pisownię, gramatykę czy dobór słow, skomentuj to!

Rust jest znany z wolnego czasu kompilacji. Spędziłem dużo czasu próbując poprawić przyrostowe czasy kompilacji testowania dla mojego projektu git-branchless w https://github.com/arxanas/git-branchless/pull/650. Oto dyskusja wyników.

Streszczenie wykonawcze

  • “Przyrostowe testowania” odnosi się tylko do zmiany kodu testu integracyjnego i przebudowanie. Kod źródłowy pozostaje niezmienony.
  • Ostatecznie udało mi się skrócić przyrostowy czas testowania z ~6.9sek do ~1.7sek (~4x). Inne sposoby na skrócenie czasu kompilacji przyniosły niewielką lub żadną poprawę.

W celach informacyjnych, oto najlepsze artykuły dotyczące konceptualnego zrozumienia modelu kompilacji Rust i poprawy czasu kompilacji:

Szczegóły projektu

Oto jak duży był mój projekt git-branchless przed pull request:

  • git-branchless/src: 12060 linii.
  • git-branchless/tests: 12897 linii.
    • Zauważ, że w dużej mierze opiera się na “snapshot testing”, więc większość tych linii kodu to “multiline string literals”.
  • git-branchless-lib/src: 12406 linii.

Dopuszczalne są długie czasy kompilacji i konsolidacja, ale aby zoptymalizować sprzężenie zwrotnego programowania, potrzebuję krótkich czasów testowania. W szczególności chcę iterować nad pracowaniem pewnego testu. (IntelliJ ma fajną funkcję automatycznego ponownego uruchamiania danego testu, gdy występuje zmiana w kodu źródła ale trwanie zbyt długo ponownej kompilacji zmniejsza jej przydatność.)

Na początek zbudowanie binary test_amend (który testuje subcommand amend) po dodaniu pojedynczego komentarza do jego pliku testu (bez zmian w library!) wymaga ~6.9sek:

$ hyperfine --warmup 3 --prepare 'echo "// @nocommit test" >>git-branchless/tests/command/test_amend.rs' 'cargo test --test mod --no-run'   
Benchmark 1: cargo test --test mod --no-run
  Time (mean ± σ):      6.927 s ±  0.123 s    [User: 7.652 s, System: 1.738 s]
  Range (min … max):    6.754 s …  7.161 s    10 runs

Tragedia! To nie duży projekt, a zmieniamy tylko testy, więc nie powinno to wymagać tak długiego czasu iteracji.

Podzielenie się na więcej crates

W pull request wyodrębniłem do dodatkowych dziewięciu crates, co daje przyrostowy czas kompilacji testu wynoszący ~1.7sek (~4x poprawa).

$ hyperfine --warmup 3 --prepare 'echo "// @nocommit test" >>git-branchless/tests/test_amend.rs' 'cargo test --test test_amend --no-run'   
Benchmark 1: cargo test --test test_amend --no-run
  Time (mean ± σ):  	1.771 s ±  0.012 s	[User: 1.471 s, System: 0.330 s]
  Range (min … max):	1.750 s …  1.793 s	10 runs

Jeśli chodzi o poprawę względną, to znaczna, ale jeśli chodzi o poprawę bezwzględną, sądzę, że jest raczej słaba. Bym oczekiwał 100-200ms na parsing, macro expansion, typechecking, i code generation (bez optimacji) dla pliku tego rozmiaru (~1k linii, głownie długi wartości łańcuchowe).

Dodatkowe, dzielenie na wiele crates utrudnia rozpowszechnianie projektu za pośrednictwem crates.io:

  • Każdy crate wymaga indywidualnego publikowania na crates.io (nie można opublikować crate z zależnościami Git w crates.io).
    • Dlatego muszę zarządzać wersjami i licencji każdego crate.
  • Według mojej ankiety wśród użytkowników, większość moich użytkowników instaluje program za pomocą cargo. Na pytanie “Jak zainstalowałeś git-branchless?”, odpowiedzi są następujące (stan na ):
    • 7/18 (38.9%): przez cargo install git-branchless.
    • 4/18 (22.2%): za pośrednictwem tradycyjnego systemowego menedżera pakietów.
    • 4/18 (22.2%): przez Nix lub NixOS
    • 2/18 (11.1%): klonując repozytorium i ręcznie budując i instalując.
    • 1/18 (5.6%): przez artefakt kompilacji z GitHub Actions

Niestety, muszę publicznie udostępniać wewnętrzne moduły na crates.io tylko po to, aby uzyskać rozsądne czasy kompilacji.

Czas bez operacji

Wtedy, mierzę czas bez operacji [ang. “no-op”] na ~350ms dla mniejszej testowe crate z niewieloma zależnościami:

$ hyperfine --warmup 3 'cargo test -p git-branchless-test --no-run' 		 
Benchmark 1: cargo test -p git-branchless-test --no-run
  Time (mean ± σ):     344.2 ms ±   3.5 ms    [User: 246.2 ms, System: 91.9 ms]
  Range (min … max):   340.4 ms … 351.0 ms    10 runs

To jest nieoczekiwane. Spodziewałbym się, że czas na kompilację bez operacji będzie podobny do git status, może 15ms:

$ hyperfine --warmup 3 'git status'
Benchmark 1: git status
  Time (mean ± σ): 	 13.5 ms ±   2.5 ms    [User: 4.9 ms, System: 6.1 ms]
  Range (min … max):    11.1 ms …  24.7 ms    197 runs

Tu może być jakiś głębszy problem. Dokumentacja cargo nextest ostrzega, że niektóre systemy przeciw malware mogą wprowadzać sztuczne opóźnienie uruchamiania podczas sprawdzania plików wykonywalnych:

A typical sign of this happening is even the simplest of tests in cargo nextest run taking more than 0.2 seconds.

Zgodnie z dokumentacją, oznaczyłem moje oprogramowanie terminala jako “Developer Tools” w systemie macOS, ale nie udało mi się skrócić czasu kompilacji bez operacji.

Koniec profilowania?

Spróbowalem z subcommand crate, który niedawno stworzyłem bez wielu zaleźności:

$ hyperfine --warmup 3 --prepare 'echo "// @nocommit test" >>git-branchless-test/tests/test_test.rs' 'cargo test -p git-branchless-test --no-run'
Benchmark 1: cargo test -p git-branchless-test --no-run
  Time (mean ± σ):  	1.855 s ±  0.034 s	[User: 1.476 s, System: 0.335 s]
  Range (min … max):	1.831 s …  1.939 s	10 runs

Wykres czasu kompilacji według cargo build nie pomaga. Tylko pokazuje to, że budowany test wymaga 100% czasu:

The timing graph for building the `git-branchless-test` crate.

The timing graph for building the git-branchless-test crate.

Koniec kolejnych pomysłów?

Oto niektóre pomysły, które nie zadziałały:

  • Połączenie testów integracyjnych w jeden binary. (W każdym razie chętnie uruchamiam indywidualne binaries, jeśli to konieczne.)
  • Zmniejszenie głównych monomorfozacji (wywołania AsRef itp. Pojawiły się w cargo llvm-lines, ale ich zmniejszenie nie zdawało się poprawiać czasu kompilacji).
  • Używanie sccache.
  • Używanie cargo nextest.
  • Używanie zld/mold/sold.
  • Ustawienie profile.dev.split-debuginfo = “unpacked” (dla systemu macOS).
  • Ustawienie profile.dev.build-override.opt-level = 3.
    • Niestety, jest dużo makr proceduralnych, zwłaszcza #[instrument] z crate tracing.
  • Ustawienie profile.dev.debug = 0. W rzeczywistości skraca czas kompilacji o 20ms, ale samo w sobie nie wystarczy.

Więc teraz utknąłem. To najwięcej, jak mogę skrócić przyrostowe czasy testowania Rust. Daj znać, jeśli masz jakieś inne pomysły.

Powiązane posty

Poniżej znajduje się kilka ręcznie wybranych postów, które mogą Cię zainteresować.

Chcesz zobaczyć więcej moich postów? Obserwuj mnie na Twitterze albo subskrybuj za pomocą RSS.

Komentarze