Idiom _ftol w 32-bitowym Delphi istnieje za sprawą plików obiektowych C. Gdy jednostka linkuje pliki .obj wyprodukowane przez 32-bitowy kompilator C, obiekty te przy każdym rzutowaniu liczby zmiennoprzecinkowej na całkowitą wywołują helper środowiska _ftol, z wartością pozostawioną na szczycie stosu x87 w ST(0), a nie przekazaną jako parametr. Zamiennik w Delphi deklaruje więc atrapę parametru Double, zdejmuje ST(0) do niego dwuwierszowym blokiem wstawki asemblerowej, a potem obcina go już w Pascalu. Pod DCC32 budowało się to dobrze przez długi czas i dokładnie dlatego wylądowało w tak wielu starszych jednostkach graficznych i PDF bez pytania nikogo o zdanie
Wystarczy przełączyć cel kompilacji na 64-bitowy, a kompilator kończy pracę błędem E1025 Unsupported language feature: 'ASM'. To nie jest ostrzeżenie o zgodności. Oznacza, że DCC64 w ogóle nie skompiluje tej procedury, niezależnie od tego, jak dobrze ten asembler działał wcześniej
32-bitowy oryginał zwykle wyglądał mniej więcej tak:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Ten blok asm wewnątrz ciała Pascalowego begin...end to dokładnie to, czego DCC64 odmawia. Oba kompilatory mają różne reguły dotyczące tego, gdzie wolno umieszczać asembler, i ta granica ma znaczenie
Dlaczego DCC64 wyznacza granicę inaczej
DCC32 dopuszcza asembler inline wewnątrz zwykłych procedur Pascalowych. Kompilator zna 32-bitową konwencję wywołań i potrafi rozumować, gdzie żyją zmienne lokalne i parametry, więc toleruje fragmenty asemblera sięgające po nazwie do ramki stosu. DCC64 przyjmuje bardziej rygorystyczne stanowisko: asembler musi znajdować się w dedykowanej funkcji asemblerowej, w której całe ciało jest asemblerem, a konwencja wywołań jest obsługiwana jawnie. Mieszanie Pascala z asemblerem nie jest w ogóle wspierane
Powód leży w architekturze. W 64-bitowej konwencji wywołań systemu Windows (Microsoft ABI) pierwsze cztery parametry docierają w rejestrach RCX, RDX, R8 i R9 dla typów całkowitych, albo w XMM0 do XMM3 dla zmiennoprzecinkowych. W normalnym przekazywaniu parametrów FPU x87 w ogóle nie uczestniczy; x87 jest technicznie dostępny, ale ABI nie używa go do transportu argumentów. Asembler zakładający, że wartość jest „na stosie FPU”, rozumuje o stanie, którego 64-bitowe ABI nigdy nie tworzy
A więc stary fragment nie ma jedynie problemu składniowego. Nawet gdyby DCC64 go zaakceptował, założenia dotyczące rejestrów byłyby błędne
Napisanie poprawnej wersji asemblerowej dla 64 bitów
Najpierw sprawdź, czy 64-bitowy _ftol jest w ogóle potrzebny. Kompilatory C dla Win64 — MSVC i oparte na Clang narzędzia C++Buildera — konwertują liczby zmiennoprzecinkowe na całkowite inline instrukcjami SSE2 i nie emitują wywołań _ftol, więc 64-bitowe pliki obiektowe rzadko odwołują się do tego symbolu. Gdy naprawdę trzeba wyeksportować symbol _ftol z konwencją cdecl dla zgodności binarnej, funkcję trzeba napisać jako czystą procedurę asemblerową. W ABI 64-bitowym parametr Double przychodzi w XMM0, a wynik Integer wraca w EAX, czyli dolnej połówce RAX. Dyrektywa .NOFRAME mówi DCC64, że procedura sama zarządza swoim stosem, co dla tak krótkiej funkcji liścia jest odpowiednie:
function _ftol: Integer; cdecl;
// Wartość typu Double oczekiwana w XMM0 zgodnie z 64-bitowym ABI
asm
.NOFRAME
cvttsd2si eax, xmm0 // obcięcie do 32-bitowej liczby całkowitej, wynik w eax
end;
CVTTSD2SI to instrukcja SSE2 do konwersji liczby zmiennoprzecinkowej podwójnej precyzji na liczbę całkowitą ze znakiem, z obcięciem w kierunku zera, czyli dokładnie to, co ma robić _ftol. Jest to jedna instrukcja, która pobiera parametr dokładnie stamtąd, gdzie zostawiło go ABI, i umieszcza wynik tam, gdzie ABI go oczekuje. Żadnego żonglowania słowem kontrolnym FPU
32-bitowy rejestr docelowy ma znaczenie. Jeśli wejście wykracza poza zakres 32-bitowej liczby całkowitej ze znakiem, CVTTSD2SI z EAX jako rejestrem docelowym zwraca wartość integer indefinite ($80000000). Zapis do RAX dałby konwersję 64-bitową, a wejścia z zakresu między 231 i 263 wróciłyby jako cokolwiek znajdzie się w ich dolnych 32 bitach. To zachowanie jest identyczne jak x87 fistp przy wejściu poza zakres. Warto potwierdzić, czy kod wywołujący może produkować takie wartości, zanim ogłosisz migrację za skończoną
Kiedy Trunc jest lepszą odpowiedzią
Powyższa wersja asemblerowa opłaca się napisać tylko wtedy, gdy istnieje faktyczny wymóg zgodności binarnej: jakiś zewnętrzny wywołujący oczekuje symbolu _ftol z konkretną konwencją wywołań, a tych wywołujących nie da się zmienić. Taka sytuacja jest rzadka. Najczęściej _ftol był prywatnym pomocnikiem używanym wyłącznie w tej samej unit, i nie ma żadnej zewnętrznej zależności od jego nazwy ani konwencji
W takim przypadku zastępuje się go zwykłym Pascalem:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc obcina w kierunku zera, co odpowiada temu, co robił _ftol przy słowie kontrolnym x87 ustawionym na tryb obcinania. Kompiluje się na DCC32 i DCC64 bez żadnych zmian. Kompilator generuje odpowiednią instrukcję dla każdej platformy docelowej: na x64 zwykle wyemituje i tak CVTTSD2SI, tę samą instrukcję co wersja pisana ręcznie. Otrzymuje się identyczne zachowanie, bez warunków platformowych i bez asemblera do utrzymywania
Jedna różnica semantyczna, którą warto sprawdzić: Trunc w domyślnej konfiguracji Delphi zgłasza wyjątek EInvalidOp, gdy dane wejściowe to NaN lub nieskończoność. Oryginalny x87 fistp po prostu zapisywał wzorzec bitowy, niczego nie zgłaszając. Jeśli kod przekazuje do tej funkcji nietypowe wartości zmiennoprzecinkowe, a stare zachowanie było ciche, należy zabezpieczyć się przy pomocy IsNaN i IsInfinite z Math przed wywołaniem Trunc
Kompilacja warunkowa, gdy oba cele pozostają aktywne
Niektóre projekty muszą nadal wydawać zarówno pliki binarne 32-bitowe, jak i 64-bitowe. Jeśli oryginalną wersję asemblerową trzeba zachować dla 32 bitów, a dla 64 bitów dostarczyć nową implementację, używa się warunku CPUX64:
function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
Result := Trunc(f);
{$ELSE}
// ścieżka 32-bitowa: DCC32 akceptuje asemblera inline
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
{$ENDIF}
end;
To jest minimalna mechaniczna poprawka i warto traktować ją jako tymczasową. Baza kodu niosąca asembler specyficzny dla architektury w pomocniku, którego jedynym celem jest obcięcie liczby zmiennoprzecinkowej do całkowitej, niesie ze sobą niepotrzebny dług. Gałąź 32-bitowa może całkowicie zniknąć, gdy tylko potwierdzi się, że nic nie zależy od efektów ubocznych FPU starej implementacji
Jeśli funkcja pojawia się w komponencie używanym w wielu unitach, przed podjęciem decyzji o sposobie migracji warto przeszukać całą bazę kodu w poszukiwaniu _ftol. Symbol o tej nazwie może być zadeklarowany w więcej niż jednym miejscu; linker wybiera jeden i po cichu ignoruje pozostałe, co oznacza, że można poprawić jedną kopię, a i tak linkować się z inną, której nikt nie dotknął