Τεχνικό Άρθρο

Μεταφορά inline asm του _ftol από 32-bit Delphi στον DCC64

Ο αρχικός ιδιωματισμός _ftol στη 32-bit Delphi μοιάζει με έξυπνο μονόγραμμο: μια συνάρτηση-περιτύλιγμα σε Pascal που βουτά σε inline assembly για να χειριστεί τη λέξη ελέγχου της x87 FPU, να αποκόψει την τιμή στη στοίβα της FPU και να βγάλει το αποτέλεσμα. Μεταγλωττιζόταν μια χαρά με τον DCC32 για πολύ καιρό, και ακριβώς γι αυτό κατέληξε σε τόσες παλιότερες μονάδες γραφικών και PDF χωρίς κανείς να το αμφισβητήσει

Αλλάξτε τον στόχο μεταγλώττισης σε 64-bit και ο μεταγλωττιστής τερματίζει με E1025 Unsupported language feature: 'ASM'. Αυτό το σφάλμα δεν είναι προειδοποίηση συμβατότητας. Σημαίνει ότι ο DCC64 δεν πρόκειται να μεταγλωττίσει καθόλου τη ρουτίνα, ανεξάρτητα από το πόσο καλά δούλευε η assembly πριν

Το 32-bit πρωτότυπο συνήθως έμοιαζε κάπως έτσι:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

Αυτό το μπλοκ asm μέσα σε ένα σώμα begin...end της Pascal είναι ακριβώς αυτό που αρνείται ο DCC64. Οι δύο μεταγλωττιστές έχουν διαφορετικούς κανόνες για το πού επιτρέπεται η assembly, και το όριο έχει σημασία

Γιατί ο DCC64 τραβά τη γραμμή αλλού

Ο DCC32 επιτρέπει inline assembly μέσα σε συνηθισμένες ρουτίνες Pascal. Ο μεταγλωττιστής γνωρίζει τη σύμβαση κλήσης των 32 bit και μπορεί να συλλογιστεί πού βρίσκονται οι τοπικές μεταβλητές και οι παράμετροι, οπότε ανέχεται αποσπάσματα assembly που φτάνουν στο πλαίσιο στοίβας με το όνομα. Ο DCC64 παίρνει αυστηρότερη θέση: η assembly πρέπει να βρίσκεται σε αποκλειστική συνάρτηση assembler, μία όπου ολόκληρο το σώμα είναι assembly και η σύμβαση κλήσης αντιμετωπίζεται ρητά. Ο συνδυασμός Pascal συν asm δεν υποστηρίζεται καθόλου

Ο υποκείμενος λόγος είναι αρχιτεκτονικός. Στη σύμβαση κλήσης των 64-bit Windows (Microsoft ABI), οι πρώτες τέσσερις παράμετροι φτάνουν στους RCX, RDX, R8 και R9 για ακέραιους τύπους, ή στους XMM0 έως XMM3 για κινητή υποδιαστολή. Δεν εμπλέκεται καθόλου η x87 FPU στο κανονικό πέρασμα παραμέτρων· η x87 είναι τεχνικά διαθέσιμη αλλά το ABI δεν τη χρησιμοποιεί για τη μεταφορά ορισμάτων. Assembly που υποθέτει ότι μια τιμή βρίσκεται «στη στοίβα της FPU» συλλογίζεται πάνω σε μια κατάσταση που το 64-bit ABI δεν δημιουργεί ποτέ

Άρα το παλιό απόσπασμα δεν έχει απλώς πρόβλημα σύνταξης. Ακόμη κι αν ο DCC64 το δεχόταν, οι υποθέσεις για τους καταχωρητές θα ήταν λανθασμένες

Διάγραμμα Delphi που συγκρίνει τον DCC32, ο οποίος μεταγλωττίζει inline asm μέσα σε ρουτίνες Pascal, με τον DCC64, ο οποίος ματαιώνει με E1025 και επιτρέπει μόνο καθαρές συναρτήσεις assembler υπό το Win64 ABI
Ο DCC32 ανέχεται ένα μπλοκ asm μέσα σε ρουτίνα Pascal ενώ ο DCC64 απαντά με E1025, επειδή το Win64 ABI μετακινεί τα ορίσματα σε καταχωρητές αντί για τη στοίβα x87

Γράφοντας μια σωστή 64-bit έκδοση σε assembler

Όταν πράγματι χρειάζεστε να εξάγετε ένα σύμβολο _ftol με σύμβαση cdecl για δυαδική συμβατότητα, η συνάρτηση πρέπει να γραφτεί ως καθαρή ρουτίνα assembler. Υπό το 64-bit ABI μια παράμετρος Double φτάνει στον XMM0, και το ακέραιο αποτέλεσμα πρέπει να βρίσκεται στον RAX κατά την επιστροφή. Η οδηγία .NOFRAME λέει στον DCC64 ότι η ρουτίνα διαχειρίζεται τη δική της στοίβα, κάτι κατάλληλο για μια τόσο σύντομη φυλλική συνάρτηση:

function _ftol: Integer; cdecl;
// τιμή Double αναμένεται στον XMM0 σύμφωνα με το 64-bit ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // αποκοπή σε ακέραιο, αποτέλεσμα στον rax
end;

Η CVTTSD2SI είναι η εντολή SSE2 για τη μετατροπή ενός αριθμού κινητής υποδιαστολής διπλής ακρίβειας σε προσημασμένο ακέραιο με αποκοπή προς το μηδέν, που είναι ακριβώς αυτό που υποτίθεται ότι κάνει το _ftol. Είναι μία εντολή, παίρνει την παράμετρο κατευθείαν από εκεί που την άφησε το ABI, και τοποθετεί το αποτέλεσμα εκεί που το περιμένει το ABI. Δεν χρειάζεται κανένα ταχυδακτυλουργικό με τη λέξη ελέγχου της FPU

Σημειώστε ότι αν η είσοδος ξεπερνά το εύρος ενός προσημασμένου ακεραίου 32 bit, η CVTTSD2SI επιστρέφει την ακέραια απροσδιόριστη τιμή ($80000000). Αυτή είναι η ίδια συμπεριφορά με την fistp της x87 σε είσοδο εκτός εύρους. Το αν οι καλούντες σας μπορούν να παραγάγουν τέτοιες τιμές αξίζει να επιβεβαιωθεί πριν κηρύξετε τη μετάβαση ολοκληρωμένη

Delphi: διάγραμμα διαδρομής δεδομένων που δείχνει μια παράμετρο Double να φτάνει στον XMM0 και να μετατρέπεται από μία εντολή CVTTSD2SI σε αποκομμένο ακέραιο που επιστρέφεται στον RAX, με τιμές εκτός εύρους να δίνουν το ακέραιο απροσδιόριστο μοτίβο
Υπό τον DCC64 το double φτάνει στον XMM0 και η CVTTSD2SI τοποθετεί το αποκομμένο αποτέλεσμα των 32 bit στον RAX ως μία μόνο εντολή SSE2

Πότε το Trunc είναι η καλύτερη απάντηση

Η παραπάνω έκδοση σε assembler αξίζει να γραφτεί μόνο όταν έχετε πραγματική απαίτηση δυαδικής συμβατότητας: κάποιος εξωτερικός καλών περιμένει το σύμβολο _ftol με συγκεκριμένη σύμβαση κλήσης, και δεν μπορείτε να αλλάξετε αυτούς τους καλούντες. Αυτή η κατάσταση είναι ασυνήθιστη. Τις περισσότερες φορές, το _ftol ήταν ένας ιδιωτικός βοηθός που χρησιμοποιούνταν μόνο μέσα στην ίδια μονάδα, και δεν υπάρχει καμία εξωτερική εξάρτηση από το όνομα ή τη σύμβασή του

Για αυτή την περίπτωση, αντικαταστήστε το με απλή Pascal:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

Η Trunc αποκόπτει προς το μηδέν, κάτι που ταιριάζει με αυτό που έκανε το _ftol με τη λέξη ελέγχου της x87 ρυθμισμένη σε λειτουργία αποκοπής. Μεταγλωττίζεται σε DCC32 και DCC64 χωρίς τροποποίηση. Ο μεταγλωττιστής παράγει την κατάλληλη εντολή για κάθε στόχο: στην x64 συνήθως θα εκπέμψει ούτως ή άλλως CVTTSD2SI, την ίδια εντολή με τη χειρόγραφη έκδοση. Παίρνετε πανομοιότυπη συμπεριφορά, χωρίς συνθήκες πλατφόρμας και χωρίς assembly προς συντήρηση

Η μία σημασιολογική διαφορά που αξίζει να ελέγξετε: η Trunc εγείρει εξαίρεση EInvalidOp στην προεπιλεγμένη διαμόρφωση της Delphi όταν η είσοδος είναι NaN ή άπειρο. Η fistp της x87 στον αρχικό κώδικα απλώς έγραφε ένα μοτίβο bit χωρίς να εγείρει τίποτα. Αν ο κώδικάς σας τροφοδοτεί ασυνήθιστες τιμές κινητής υποδιαστολής σε αυτή τη συνάρτηση και η παλιά συμπεριφορά ήταν σιωπηλή, προστατευθείτε με IsNaN και IsInfinite από τη Math πριν καλέσετε την Trunc

Διάγραμμα απόφασης σε Delphi για τη μεταφορά του _ftol: αντικαταστήστε το με απλό Trunc όταν κανένας εξωτερικός καλών δεν χρειάζεται το σύμβολο cdecl, αλλιώς κρατήστε μια καθαρή εξαγωγή assembler, και προστατέψτε τις εισόδους NaN ή απείρου από το EInvalidOp
Μεταβείτε σε απλό Trunc εκτός αν εξωτερικοί καλούντες εξαρτώνται από το εξαγόμενο σύμβολο, και προστατέψτε τις εισόδους NaN ή απείρου από το EInvalidOp έτσι κι αλλιώς

Υπό συνθήκη μεταγλώττιση όταν παραμένουν ενεργοί και οι δύο στόχοι

Ορισμένα έργα πρέπει να συνεχίσουν να διανέμουν και 32-bit και 64-bit δυαδικά. Αν η αρχική έκδοση σε assembler πρέπει να διατηρηθεί για τα 32 bit και να δοθεί νέα υλοποίηση για τα 64 bit, χρησιμοποιήστε τη συνθήκη CPUX64:

function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
  Result := Trunc(f);
{$ELSE}
  // διαδρομή 32 bit: ο DCC32 δέχεται inline asm
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
{$ENDIF}
end;

Αυτή είναι η ελάχιστη μηχανική διόρθωση, και αξίζει να την αντιμετωπίσετε ως προσωρινή. Μια βάση κώδικα που κουβαλά assembly ειδική ανά αρχιτεκτονική μέσα σε έναν βοηθό του οποίου ο μοναδικός σκοπός είναι η αποκοπή από κινητή υποδιαστολή σε ακέραιο κουβαλά περιττό χρέος. Ο κλάδος των 32 bit μπορεί να φύγει εντελώς μόλις επιβεβαιώσετε ότι τίποτα δεν εξαρτάται από τις παρενέργειες της παλιάς υλοποίησης στην FPU

Αν η συνάρτηση εμφανίζεται σε ένα στοιχείο που χρησιμοποιείται σε πολλές μονάδες, ψάξτε ολόκληρη τη βάση κώδικα για _ftol πριν αποφασίσετε πώς θα κάνετε τη μετάβαση. Ένα σύμβολο με αυτό το όνομα μπορεί να δηλώνεται σε περισσότερα από ένα σημεία· ο linker διαλέγει ένα και αγνοεί σιωπηλά τα υπόλοιπα, πράγμα που σημαίνει ότι μπορεί να διορθώσετε το ένα αντίγραφο και να συνδέεστε ακόμη με ένα άλλο που δεν το αγγίξατε