Włos mi się zjeżył na głowie jak przeczytałem o przydzielaniu pamięci dla pojedynczych znaków. Zgroza. (No chyba, że czegoś tu nie wiem).
Od jakiegoś czasu używam prywatnie parsera formatu MaSzyny do swoich celów (robię nowy, lepszy starter) - działa na zasadzie FSM-Lexer/Parser. Na razie wszystko mu jedno co jest w tych plikach, działa jako iterator, czyli od podpiętego modułu zależy, co do czego będzie parsował. Starter interesuje tylko niewielki podzbiór, więc ten podzbiór jest czytany, cała reszta jest ignorowana, w momencie jak przestanę ją ignorować, nie będą mnie obchodzić stringi w skryptach - dostanę gotowe obiekty. Przypuśćmy, że mam obiekt składający się po prostu ze współrzędnych X, Y, Z. Czyli strukturę z 3 double. Funkcja parsera "ParseVector()" pobierze 3 tokeny, sparsuje 3 stringi jako double i zapisze w strukturze. Do tego jest jeszcze parsowanie kolorów RGB, ścieżek do plików i czasów.
Jak tam to działa? Masz jeden blok pamięci z całym skryptem. Lexer analizuje za każdym krokiem 1 do 2 znaków skryptu. Znaczenie znaku jest interpretowane w zależności od stanu (int). Stan zmienia się w zależności od napotkanych znaków. Szybciej się nie da. Pierwsza faza (analiza leksykalna) rozdziela logicznie skrypt na "encje" (powiedzmy znaczące słowa), znaki rozdzielające i komentarze. Wynikiem tej fazy są wyłącznie offsety i długości w skrypcie. Biorąc "string" z takiego tokena z góry znasz jego długość. Dodatkowo każdy taki string możesz praktycznie zapisać dokładnie w tym samym przydzielonym 1KB (dałbym nawet 64KB, żeby zapas był). Oczywiście nie unikniesz "ToDouble()", ale przynajmniej nie będzie to zużywać bez porównania większego czasu na przydzielanie pamięci. Z doświadczenia wiem, że mało rzeczy jest wolniejszych od przydzielania. Jak pętla ma przejść po 100MB danych, w środku nie może być praktycznie żadnego przydzielania.
Gdybym potrzebował wydajności (optymalizacja czasu ładowania) - zapisałbym wynikowe obiekty jako binarki. Przykładowo "nazwa.scm => nazwa.bin", albo "katalog/nazwa.scm => "cache/nazwa.bin". I po krzyku. Pierwsze uruchomienie - ładowanie 5 minut, drugie uruchomienie ładowanie 10 sekund. Zastanowiłbym się nawet nad przepuszczeniem strumienia cache przez kompresor. Być może byłoby to szybsze niż ładowanie dłuższych plików nieskompresowanych.
Do tego nie trzeba n "własnych formatów". Dokładnie jeden ogólny. Wynikający ze struktury binarnej obiektu. Format definiuje struktura. Serializację gotowy serializer. Broń boziu pisać własny, szkoda czasu. Pierwsza faza ładuje, parsuje i cache-uje pliki. Dalej w kodzie nie powinno być już kompletnie nic związanego z parsowaniem. Koniecznie osobne przebiegi. Nie "parsuj linię, załaduj teksturę, parsuj kolejną", tylko "załaduj wszystkie skrypty (łącznie z include), przejdź do ładowania pozostałych zasobów". Mam nadzieję, że rozkłady jazdy mają wewnętrznie jakąś sensowną binarną reprezentację, bo parsowanie tych koszmarków "na żywca" musi być bardzo wolne.
Dla optymizacji można by pokusić się o własną wersję "ToDouble" - ignorującą masę przypadków brzegowych jak różne formaty liczb. Ale to słaba opcja, lepiej dopuścić w skryptach bardziej "poluzowaną" notację kosztem wydajności, a zapisywać binarki w cache. Do zrobienia: 1 ogólny format cache, testowanie aktualności przez porównanie dat plików.