Κονσόλα τσέπης

Κονσόλα τσέπης

Άσκηση εκμάθησης

Εισαγωγή

Κώδικας χωρίς διακλαδώσεις

Η έννοια των συνθηκών παρουσιάζει το jcc, την οικογένεια εντολών για διακλάδωση με βάση μια συνθήκη. Μια εντολή jcc μεταφέρει την εκτέλεση σε άλλο σημείο του προγράμματος μόνο αν ικανοποιείται η συνθήκη της.

Ωστόσο, οι σύγχρονοι επεξεργαστές είναι γρήγοροι και μπορούν να εκτελούν πολλές εντολές παράλληλα. Θα κόστιζε πολύ να σταματήσει όλη η εργασία όσο ο επεξεργαστής περιμένει έναν έλεγχο συνθήκης σε χρόνο εκτέλεσης.

Γι' αυτό, εσωτερικά, ο επεξεργαστής δεν περιμένει. Χρησιμοποιεί έναν προβλέπτη διακλαδώσεων για να μαντέψει το αποτέλεσμα μιας συνθήκης και ξεκινά να εκτελεί κατ' εικασία τη διαδρομή που προέβλεψε.

Αν η πρόβλεψη είναι σωστή, η εκτέλεση συνεχίζεται σαν να μην υπήρχε διακλάδωση. Ωστόσο, αν η πρόβλεψη είναι λανθασμένη, ο επεξεργαστής πρέπει να απορρίψει την εργασία που έγινε στη διαδρομή που προέβλεψε και να ξεκινήσει από την αρχή στη σωστή.

Αυτό ονομάζεται λανθασμένη πρόβλεψη διακλάδωσης και είναι ακριβό, καθώς προκαλεί καθυστέρηση.

Όταν μια διακλάδωση είναι δύσκολο να προβλεφθεί, αυτές οι λανθασμένες προβλέψεις αθροίζονται. Σε αυτές τις περιπτώσεις, είναι καμιά φορά προτιμότερο να αποφεύγεται εντελώς η διακλάδωση, ώστε να εκτελούνται οι ίδιες εντολές ανεξάρτητα από τη συνθήκη. Ο κώδικας που επιλέγει μεταξύ τιμών χωρίς jcc ονομάζεται κώδικας χωρίς διακλαδώσεις.

Υπάρχουν αρκετοί τρόποι να δομήσεις τον κώδικα ώστε να διακλαδίζεται πιο σπάνια, ή ώστε οι διακλαδώσεις που έχει να είναι πιο προβλέψιμες. Συγκεκριμένα, το x86-64 παρέχει δύο οικογένειες εντολών που χρησιμοποιούνται συχνά για τη συγγραφή κώδικα χωρίς διακλαδώσεις.

Υπό συνθήκη μεταφορές

Η οικογένεια εντολών cmovcc αντιγράφει τον τελεστέο προέλευσης στον τελεστέο προορισμού μόνο αν ικανοποιείται μια συγκεκριμένη συνθήκη. Διαφορετικά, ο τελεστέος προορισμού μένει αμετάβλητος.

Η κατάληξη cc ακολουθεί την ίδια ονοματολογία όπως στο jcc, με την ίδια σημασία:

εντολή μεταφορά αν
cmove A == B μετά από cmp A, B
cmovne A != B μετά από cmp A, B
cmovl A < B (με πρόσημο) μετά από cmp A, B
cmovb A < B (χωρίς πρόσημο) μετά από cmp A, B
cmovg A > B (με πρόσημο) μετά από cmp A, B
cmova A > B (χωρίς πρόσημο) μετά από cmp A, B

Οι κανόνες είναι παρόμοιοι με του jcc:

  1. Τα l και g χρησιμοποιούνται για συγκρίσεις με πρόσημο, και τα b και a για συγκρίσεις χωρίς πρόσημο.
  2. Η προσθήκη του e σε μια κατάληξη συμπεριλαμβάνει την ισότητα.
  3. Η προσθήκη του n αναιρεί τη συνθήκη.
  4. Υπάρχουν επίσης παραλλαγές που αναφέρονται στο ότι έχει τεθεί η σημαία. Για παράδειγμα, τα cmovz και cmovc.

Θεώρησε την απόλυτη τιμή ενός ακέραιου με πρόσημο στον rdi, που επιστρέφεται στον rax. Γραμμένη με jcc, η συνάρτηση επιλέγει μία από δύο διαδρομές:

abs_branch:
    mov rax, rdi
    cmp rax, 0
    jge .done
    neg rax
.done:
    ret

Γραμμένη με cmovcc, και οι δύο υποψήφιες τιμές υπολογίζονται άνευ όρων και η υπό συνθήκη μεταφορά διαλέγει μία:

abs_branchless:
    mov rax, rdi
    neg rax            ; rax = -rdi
    cmp rdi, 0
    cmovge rax, rdi    ; the value in `rdi` is moved to `rax` if rdi >= 0
                       ; otherwise, it stays the same, i.e., -rdi
    ; now rax = abs(rdi)
    ret

Η έκδοση χωρίς διακλαδώσεις εκτελεί πάντα τις ίδιες εντολές. Δεν υπάρχει κανένα jcc για να μαντέψει ο προβλέπτης.

Ο προορισμός ενός cmovcc πρέπει να είναι καταχωρητής. Η πηγή μπορεί να είναι καταχωρητής ή θέση μνήμης, αλλά όχι άμεση τιμή.

Note

Στον κώδικα παραπάνω, η εντολή neg θέτει αρκετές σημαίες ανάλογα με το αποτέλεσμα, συμπεριλαμβανομένης της σημαίας προσήμου (SF). Αυτό σημαίνει ότι το cmp rdi, 0 μπορεί να αφαιρεθεί εντελώς:

abs_branchless:
    mov rax, rdi
    neg rax            ; rax = -rdi. `neg` sets SF = 1 if the result is negative
    cmovs rax, rdi     ; if SF == 1 (i.e., rax < 0), replace rax with rdi
                       ; otherwise rax stays = -rdi, which is non-negative
    ; now rax = abs(rdi)
    ret

Για θετικό rdi, η neg παράγει αρνητικό αποτέλεσμα και SF == 1, οπότε η cmovs επαναφέρει το rdi. Για μηδενικό ή αρνητικό rdi, η neg παράγει μη αρνητικό αποτέλεσμα και SF == 0, οπότε ο rax κρατά την τιμή με το αντίθετο πρόσημο (η οποία είναι η σωστή απόλυτη τιμή).

Αν και το cmp είναι ο κύριος τρόπος σύγκρισης τιμών στον κώδικα assembly, πολλές εντολές θέτουν επίσης σημαίες ανάλογα με το αποτέλεσμά τους. Ο πλήρης κατάλογος των σημαιών που επηρεάζει μια εντολή αναφέρεται συνήθως στην αναφορά της. Για την neg συγκεκριμένα, αυτές είναι οι SF, ZF, CF, OF και PF.

Υπό συνθήκη εκχώρηση

Η οικογένεια εντολών setcc θέτει τον τελεστέο προορισμού σε 1 αν ικανοποιείται μια συγκεκριμένη συνθήκη, και σε 0 διαφορετικά. Ο προορισμός είναι πάντα ένας τελεστέος 8 bit.

Η κατάληξη cc ακολουθεί την ίδια ονοματολογία όπως στα jcc και cmovcc. Για παράδειγμα, η setz θέτει τον προορισμό σε 1 αν ZF == 1 και σε 0 διαφορετικά.

Επειδή ο προορισμός είναι 8 bit, συνηθίζεται να ακολουθείται η setcc από μια movzx όταν χρειάζεται μια μεγαλύτερη τιμή:

cmp rdi, rsi
setg al             ; al = 1 if rdi > rsi (signed), 0 otherwise
movzx eax, al       ; eax (and rax) = 1 or 0, with the upper bits cleared

Αυτό είναι ένα συνηθισμένο ιδίωμα για να μετατραπεί το αποτέλεσμα μιας σύγκρισης σε ακέραιο 0 ή 1.

Οδηγίες

Γράφεις firmware για το σύστημα βαθμολογίας μιας retro φορητής κονσόλας. Η CPU του μηχανήματος είναι μέτρια, και η οθόνη βαθμολογίας ανανεώνεται με σταθερό ρυθμό. Για να μένει ομαλή η εικόνα, οι ρουτίνες βαθμολογίας πρέπει να εκτελούνται σε προβλέψιμο αριθμό κύκλων ανεξάρτητα από το τι κάνει ο παίκτης.

Έχεις τέσσερις εργασίες.

Note

Ο κώδικας για τις εργασίες πρέπει να είναι χωρίς διακλαδώσεις. Χρησιμοποίησε cmovcc, setcc και αριθμητικές πράξεις αντί για υπό συνθήκη άλματα.

1. Πρόσθεσε μπόνους χωρίς υπερχείλιση της οθόνης

Η περιοχή βαθμολογίας της οθόνης LCD της κονσόλας χωράει έξι δεκαδικά ψηφία. Δεν μπορεί να εμφανίσει κανέναν αριθμό πάνω από 999999. Όταν ένα μπόνους θα έσπρωχνε το τρέχον σύνολο πάνω από αυτό το όριο, το σύνολο που εμφανίζεται κρατιέται στο μέγιστο αντί να αναδιπλωθεί.

Όρισε μια συνάρτηση add_bonus που, δεδομένου ενός τρέχοντος συνόλου και ενός μπόνους προς πρόσθεση, επιστρέφει το νέο σύνολο περιορισμένο στο 999999. Τα ορίσματα, με τη σειρά, είναι:

  1. total: το τρέχον σύνολο βαθμολογίας (πάντα μεταξύ 0 και 999999)
  2. bonus: το μπόνους προς πρόσθεση (πάντα μη αρνητικό)

Η τιμή επιστροφής είναι total + bonus αν αυτό το άθροισμα είναι μικρότερο ή ίσο με 999999, και 999999 διαφορετικά:

add_bonus(500, 100);
// => 600
add_bonus(999990, 50);
// => 999999
add_bonus(999999, 0);
// => 999999

Τόσο τα ορίσματα όσο και η τιμή επιστροφής είναι προσημασμένοι ακέραιοι 64-bit.

Note

Μπορείς να υποθέσεις ότι το total + bonus δεν υπερχειλίζει έναν προσημασμένο ακέραιο 64-bit.

2. Σύγκρινε δύο βαθμολογίες

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

Όρισε μια συνάρτηση compare_scores που, δεδομένων δύο βαθμολογιών, επιστρέφει:

  • +1 αν η πρώτη βαθμολογία είναι υψηλότερη
  • -1 αν η πρώτη βαθμολογία είναι χαμηλότερη
  • 0 αν οι δύο βαθμολογίες είναι ίσες

Παράδειγμα:

compare_scores(500, 300);
// => 1
compare_scores(300, 500);
// => -1
compare_scores(500, 500);
// => 0

Τόσο τα ορίσματα όσο και η τιμή επιστροφής είναι προσημασμένοι ακέραιοι 64-bit.

3. Επικύρωσε μια ακατέργαστη βαθμολογία

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

Όρισε μια συνάρτηση validate_score που, δεδομένης μιας ακατέργαστης βαθμολογίας και των επιτρεπτών ορίων, επιστρέφει τη βαθμολογία περιορισμένη στο [min, max]. Τα ορίσματα, με τη σειρά, είναι:

  1. score: η ακατέργαστη βαθμολογία που ελήφθη μέσω του καλωδίου σύνδεσης
  2. min: η μικρότερη επιτρεπτή βαθμολογία
  3. max: η μεγαλύτερη επιτρεπτή βαθμολογία

Η τιμή επιστροφής είναι min αν score < min, max αν score > max, και score διαφορετικά. Μπορείς να υποθέσεις ότι min <= max.

Παράδειγμα:

validate_score(450, 0, 500);
// => 450
validate_score(-50, 0, 1530);
// => 0
validate_score(1234567, 0, 2999);
// => 2999

Όλα τα ορίσματα και η τιμή επιστροφής είναι προσημασμένοι ακέραιοι 64-bit.

4. Παρακολούθησε τις δύο υψηλότερες βαθμολογίες

Στο τέλος κάθε συνεδρίας παιχνιδιού, η κονσόλα σαρώνει το αρχείο καταγραφής των πρόσφατων παιχνιδιών στο αρχείο αποθήκευσής της για να βρει τις δύο υψηλότερες βαθμολογίες που έχουν καταγραφεί ποτέ. Το αρχείο καταγραφής είναι ένας απλός πίνακας προσημασμένων ακεραίων 64-bit στη μνήμη. Η σάρωση διατρέχει τον πίνακα μία φορά και κρατάει δύο τρέχοντα μέγιστα: το υψηλότερο που έχει δει μέχρι στιγμής, και το δεύτερο υψηλότερο.

Σημείωσε ότι και οι δύο αριθμοί πρέπει να είναι τουλάχιστον 0. Κάθε αρνητικός αριθμός αγνοείται.

Μια πρώτη εκδοχή της συνάρτησης γράφτηκε με διαδοχικά υπό συνθήκη άλματα. Για κάθε υποψήφιο, ο κώδικας ρωτάει πού ταιριάζει (πάνω από το πρώτο, ανάμεσα στο πρώτο και το δεύτερο, ή πουθενά) και μετατοπίζει ανάλογα τα τρέχοντα μέγιστα, απορρίπτοντας κάθε αρνητικό αποτέλεσμα.

Αυτό δουλεύει. Ωστόσο, επειδή οι τιμές εισόδου είναι τυχαίες, κάθε άλμα υπό συνθήκη οδηγεί σε πολλές λανθασμένες προβλέψεις διακλάδωσης. Γι' αυτό αποφάσισες να ξαναγράψεις τον κύριο βρόχο χωρίς διακλαδώσεις.

Παρακάτω είναι η συνάρτηση με το τμήμα με τις διακλαδώσεις σχολιασμένο:

; rdi = output buffer for two elements, rsi = input array address, rdx = number of elements in array
top_two:
    xor r8d, r8d                   ; first  = 0
    xor r9d, r9d                   ; second = 0
    xor ecx, ecx                   ; index = 0
    test rdx, rdx
    jz .done

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;                       BRANCHY CODE TO REFACTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;.loop:
;    mov rax, qword [rsi + 8*rcx]   ; candidate
;    inc rcx
;    cmp rax, r8
;    jle .check_second              ; candidate <= first, try second
;    mov r9, r8                     ; second = first
;    mov r8, rax                    ; first  = candidate
;    cmp rcx, rdx
;    jb .loop                       ; go to next iteration
;    jmp .done                      ; otherwise, we are done
;.check_second:
;    cmp rax, r9
;    jle .loop                      ; candidate <= second, go to next iteration
;    mov r9, rax                    ; second = candidate
;    cmp rcx, rdx
;    jb .loop                       ; go to next iteration
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

.done:
    mov qword [rdi], r8            ; save first
    mov qword [rdi + 8], r9        ; save second
    ret

Αντικατέστησε το σχολιασμένο τμήμα με υλοποίηση χωρίς διακλαδώσεις. Το μόνο άλμα που επιτρέπεται στον νέο κώδικα είναι αυτό που πηγαίνει πίσω στην αρχή του βρόχου για να ελέγξει ένα νέο στοιχείο του πίνακα.

Η συνάρτηση δεν έχει τιμή επιστροφής και δέχεται τα ίδια ορίσματα με την εκδοχή με τις διακλαδώσεις:

  1. out: buffer εξόδου όπου η συνάρτηση θα γράψει τις δύο κορυφαίες μη αρνητικές βαθμολογίες σε φθίνουσα σειρά, ως προσημασμένους ακεραίους 64-bit
  2. array: πίνακας εισόδου με προσημασμένους ακεραίους 64-bit
  3. length: ο αριθμός των στοιχείων του πίνακα, ως ανυπόγραφος ακέραιος 64-bit
Note

Αν ο πίνακας περιέχει διπλότυπες τιμές, τα διπλότυπα μπορεί να εμφανιστούν και στις δύο θέσεις εξόδου αν είναι οι δύο κορυφαίες βαθμολογίες.

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα
x86-64 Assembly Exercism

Έτοιμος να ξεκινήσεις την άσκηση Κονσόλα τσέπης;

Γράψου στο Exercism για να μάθεις και να κατακτήσεις x86-64 Assembly με 22 έννοιες130 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.