programowanieProgramista Rust systemowy

Jak działa obliczanie stałych (const evaluation) w Rust? Czym const różni się od static i let, w jakich przypadkach można (i należy) używać const fn, oraz jakie są ograniczenia podczas pisania obliczeń na etapie kompilacji?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Obliczanie stałych w Rust pozwala na wykonanie części obliczeń lub inicjalizacji na etapie kompilacji, a nie w czasie wykonywania programu.

  • const deklaruje niezmienną stałą, obliczaną na etapie kompilacji, która nie ma adresu w pamięci:
const PI: f64 = 3.1415;
  • static deklaruje globalną zmienną, umieszczoną w określonym segmencie pamięci (zazwyczaj .data lub .bss), której mutowalność jest możliwa (wymaga unsafe):
static mut GLOBAL_COUNTER: i32 = 0;
  • let jest używane dla zmiennych na stosie, ich wartość może być obliczana w czasie wykonania, a muszą być zainicjalizowane przed pierwszym użyciem.

const fn to funkcja, której wynik może być użyty do nadania wartości const lub static. Takie funkcje mogą być wywoływane w kontekście stałej.

const fn factorial(n: usize) -> usize { if n == 0 { 1 } else { n * factorial(n - 1) } } const FACT_5: usize = factorial(5); // Kompiluje się!

Ograniczenia const fn:

  • Można używać tylko innych const fn,
  • Nie można używać alokacji na stercie (Box::new itd.),
  • Nie można wywoływać operacji unsafe,
  • Brak dostępu do zmiennych i funkcji zewnętrznych (jeśli nie są const fn).

Pytanie z podstępem.

Pytanie: Czy można używać dowolnej funkcji w kontekście const, jeśli jej wynik się nie zmienia? Na przykład w ten sposób:

fn add(a: i32, b: i32) -> i32 { a + b } const RES: i32 = add(1, 2);

Typowa błędna odpowiedź: Tak, ponieważ funkcja jest czysta, a wynik jest znany z góry.

Prawidłowa odpowiedź: Nie, funkcja musi być jawnie zadeklarowana jako const fn, tylko wtedy można ją wywołać wewnątrz inicjalizacji const. Zwykłe funkcje są wywoływane tylko w czasie wykonania!

Przykład:

const fn add(a: i32, b: i32) -> i32 { a + b } const RES: i32 = add(1, 2); // Kompiluje się!

Przykłady rzeczywistych błędów z powodu braku znajomości szczegółów tematu.


Historia

W projekcie z obliczeniami geometrii trójwymiarowej deweloper próbował zadeklarować tabelę wartości za pomocą wyniku wywołania zwykłej funkcji, a nie const fn. W rezultacie wystąpiły błędy kompilacji, a zysk z obliczeń w czasie kompilacji został utracony.


Historia

Użycie static mut dla globalnego cache'a doprowadziło do wyścigu danych podczas dostępu z wielu wątków (static mut nie jest bezpieczny!). Konieczne było użycie Atomic lub Mutex, aby zsynchronizować dostęp do globalnego zasobu.


Historia

W próbie przyspieszenia inicjalizacji dużych tablic zadeklarowano je jako static, ale zapomniano, że static zawsze ma stały adres, przez co dane nie trafiały do cache procesora jako lokalne, a operacje spowolniły się na gorącej ścieżce logiki serwera. Należało użyć lokalnych wyrażeń let z obliczeniami w czasie wykonania.