2026-11-08 · ← Aktualności
Zoptymalizowane C skróciło parse_number() do 19 ns. Rust wygrał tylko pierwszą rundę
Simon Willison porównał cztery rozszerzenia SQLite do parsowania zapisów liczbowych. Rust pokonał prostą wersję w C, lecz około 30 wierszy optymalizacji obniżyło wynik C do 19,2 ns na rekord i pokazało, że benchmark języków często mierzy przede wszystkim wybór parsera.
Nie udało się wczytać obrazu.
Simon Willison wykorzystał Claude Opus 5.5 do przygotowania czterech wersji funkcji SQLite parse_number(), która zamienia tekst taki jak -$1,234.56 na liczbę. Najciekawsze jest to, że nie zatrzymał się na pierwszym zaskakującym wyniku, lecz sprawdził jego przyczynę.
Rust pokonał prostą wersję w C, dopóki C korzystało z ogólnego parsera
Test objął dwie implementacje w Ruście i dwie w C. Prosta wersja w C przetwarzała mieszane dane w 67,3 ns na rekord, a Rust potrzebował od 40,5 do 45,9 ns. Dla liczb dziesiętnych różnica wzrosła do 106,3 ns wobec 46,5 do 54,5 ns.
Przyczyną nie był cały język. Wersja w C przekazywała liczby dziesiętne do ogólnej funkcji strtod(), podczas gdy Rust korzystał z szybszej ścieżki swojego parsera. Dodanie około 30 wierszy obsługujących typowe wartości obniżyło wynik zoptymalizowanego C do 19,2 ns dla danych mieszanych, 15,8 ns dla liczb całkowitych i 17,5 ns dla dziesiętnych.
Mała funkcja ujawniła koszt abstrakcji i ręcznej pracy
Rozszerzenie w C miało 14 408 B. Warianty w Ruście zajmowały 298 464 B oraz 320 864 B, ponieważ zawierały bibliotekę standardową i kod pomocniczy. Zoptymalizowane C wymagało też 104 wierszy kodu, ręcznej obsługi buforów, kontroli przepełnień i ostrożności przy obliczeniach zmiennoprzecinkowych.
Wersje w Ruście były krótsze i bezpieczniejsze do napisania. Dla programisty wybór jest konkretny: mała, często wywoływana funkcja może w C zyskać niższe opóźnienie i mniejszy plik binarny, lecz ceną jest bardziej zawiły kod i większe ryzyko błędów. Język należy więc dobrać do tego, czy ważniejsza jest każda nanosekunda, czy czas osób utrzymujących rozszerzenie.
To wąski mikrobenchmark, a nie uniwersalny pojedynek języków
Pomiary wykonano na jednej maszynie Linux x86-64 z 2 vCPU i na 2 milionach rekordów. Każdy wynik to najlepszy z 15 przebiegów po odjęciu bazowego skanowania tabeli. Metoda dobrze izoluje koszt funkcji, lecz nie pokazuje zachowania z inną biblioteką C, na innej architekturze ani w aplikacji wykonującej dodatkową pracę wokół bazy.
Raport badawczy powstał również z pomocą LLM. Wiarygodność zapewniają mu opublikowany kod, 40 ręcznych testów oraz 8 milionów porównań fuzz bez wykrytej różnicy, a nie sposób napisania tekstu. Reprodukcja na innym sprzęcie nadal znaczy więcej niż pewny wniosek z jednej tabeli.
Reprodukcja pokaże, czy przewaga przetrwa zmianę maszyny
Najcenniejszym kolejnym sygnałem będzie ten sam test na macOS, ARM i z innymi implementacjami biblioteki standardowej. Warto też usunąć alokację Vec z obu wariantów w Ruście, aby oddzielić koszt języka od kosztu warstwy bibliotecznej.
Wdrożenie produkcyjne dołoży własny sprawdzian: udział błędnych wartości, długość tekstów i częstotliwość uruchamiania wolnej ścieżki przez strtod(). Wtedy okaże się, czy 19,2 ns przetrwa kontakt z danymi, których nie przygotowano pod benchmark.
Werdykt Lilith
Rust dobiegł przed pierwszą wersją w C, lecz zoptymalizowane C zostawiło go z tyłu na 2 milionach rekordów. Przy tak małej funkcji o wyniku decyduje mierzony parser, a nie flaga języka.