Τρίτη, 12 Δεκεμβρίου 2017

Αναθεώρηση 23 (Έκδοση 9.0)

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

Σε αυτήν την αναθεώρηση μειώθηκε η χρήση του σωρού επιστροφής (process stack) του διερμηνευτή, μετά από κατάλληλες αλλαγές. Επιπλέον μπήκε σύστημα που ελέγχει το σωρό, γιατί τώρα ο διερμηνευτήγς "γνωρίζει" το μέγεθός του και πόσο έχει χρησιμοποιήσει.
Το πρόγραμμα m2000.exe που φορτώνει το m2000.dll δίνει στον διερμηνευτή 32MByte σωρό επιστροφής (ενώ 1MByte δίνει εξ ορισμού η Visual Basic, αλλά γίνεται να αλλάξει, κατά την παραγωγή του m2000.exe, στο αρχείο mexe.vbp υπάρχει το τμήμα [VBCompiler] και εκεί αλλάζουμε την τιμή του σωρού).
To m2000.exe είναι νέο, αν τρέξουμε το m2000.dll (τον διερμηνευτή δηλαδή) με παλιό m2000.exe θα έχει κατιτίς μικρότερο σωρό. Δείτε όμως μια διαφορά. Το νέο m2000.exe δίνει το μέγεθος στο m2000.dll με μήνυμα, ώστε να μην το ψάχνει. Στις άλλες περιπτώσεις το ψάχνει με τον απλό τρόπο..γεμίζει το σωρό -πολύ γρήγορα- μέχρι να βγει εσωτερικά λάθος! Σε κάθε αύξηση όμως καταγράφει το μέγεθος. Πώς όμως ξέρει ο διερμηνευτής το στιγμαίο μέγεθος (ή τωρινό θα το λέγαμε);
Καθώς ξεκινάει ο διερμηνευτής με την δημιουργία του αντικειμένου Μ2000.callback (αυτό καλεί το  m2000.exe γίνεται αυτό:
(κώδικας Visual Basic 6)
Dim anyname As Long
startaddress = VarPtr(anyname)

η anytime γράφεται στο σωρό τιμών, ενώ η startaddress είναι global, μόνιμη στην ουσία, ίσως εκτός σωρού! Αλλά αυτό που κάνουμε είναι να γράψουμε στην μόνιμη την θέση (διεύθυνση) του anyname, που είναι στο σωρό!  Με τον ίδιο τρόπο θα πάρουμε ανά πάσα στιγμή μια μέτρηση και με την διαφορά θα βρούμε το μέγεθος. Η διαφορά δεν είναι συνηθισμένη  γιατί τα νούμερα που παίρνουμε τα λογαριάζουμε ως μη προσημασμένων αριθμών. (unsigned long). Και τέτοιους αριθμούς δεν τους υποστηρίζει άμεσα η VB6, οπότε χρειάζεται μετατροπή. Μπορεί κανείς να δει το κώδικα της Μ2000 (είναι πάντα διαθέσιμος και στο git).

Πριν από αυτήν την αναθεώρηση υπήρχε ένας τρόπος απαρίθμησης κλήσεων συναρτήσεων, για να μην βγει οπουδήποτε το "out of stack", για να μην βγει καθόλου στην ουσία. Η απαρίθμηση ήταν ανά κλήση. Ενώ τώρα γίνεται κάτι άλλο. Συγκρίνεται λοιπόν το τωρινό μέγεθος με το μέγιστο.
Αυτό έδωσε το πλεονέκτημα να μπορούμε να έχουμε διαφορετικές αποδόσεις ανάλογα με τον τύπο κλήσης. Πράγματι υπάρχουν τρεις τύποι κλήσεων.
  • Η κλήση συνάρτησης μέσα από παραστάσεις. 
  • Η κλήση συνάρτησης/τμήματος με την Κάλεσε (το τμήμα έχει αναδρομή έτσι)
  • Η κλήση τμήματος από άλλο τμήμα
Στο τρίτο δεν έχουμε αναδρομή δηλαδή ένα τμήμα δεν μπορεί να καλέσει το όνομά του. Όμως αν έχουμε δυο γενικά τμήματα, μπορεί το ένα να καλεί το άλλο! Γιατί σε καθένα τμήμα θα είναι θεατό το άλλο, άρα μπορεί να κληθεί. Αυτό στην προηγούμενη αναθεώρηση ελέγχοταν όμως αριθμητικά όπως και η κλήση συνάρτησης. Όμως η κλήση αυτή είναι η πιο μικρή σε κατανάλωση σωρού, άρα για ίσο μέγεθος σωρού, δίνει περισσότερες κλήσεις σε σχέση με την κλήση συνάρτησης από παράσταση.
Με την εντολή Monitor ή Έλεγχος (στη γραμμή εντολών) θα δούμε τα Stack Limits (όρια σωρού) για τους τρείς τύπους (αν μόνο ο ένας από αυτούς κατανάλωνε όλο το σωρό)
Δίνει 3266 για αναδρομή σε συναρτήσεις, 5478 για κλήσεις με την Κάλεσε για τμήματα και συναρτήσεις, και 6419 για κλήσεις τμημάτων με το όνομά τους μόνο.

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

με edit D1 γράφουμε τα παρακάτω

global mm
function rec {
      mm++
      print mm, " ", monitor.stack
      =rec()
}
n=rec()

με edit D2 γράφουμε τα παρακάτω

global mm
function rec {
      mm++
      print mm, " ", monitor.stack
      call rec()
}
call rec()


με edit D3 γράφουμε τα παρακάτω
global mm
module global alfa1 {
      mm++
      print mm, " ", monitor.stack
      alfa2
}
module global alfa2 {
      mm++
      print mm, " ", monitor.stack
      alfa1
}
alfa1

αν το παραπάνω το κάναμε έτσι, θα πέφταμε στην περίπτωση του Call (όπως στο D2)
με edit D4 γράφουμε τα παρακάτω
global mm
module global alfa1 {
      mm++
      print mm, " ", monitor.stack
      call alfa2
}
module global alfa2 {
      mm++
      print mm, " ", monitor.stack
      call alfa1
}
call alfa1

Οι ρουτίνες δεν απασχολούν τον σωρό. Με το Όριο.Αναδρομής 0 αφαιρούμε το όριο, ή αν θέλουμε δίνουμε ένα! Οι ρουτίνες έχουν δικό τους σωρό επιστροφής, στο αντικείμενο που εκτελούνται (στο τμήμα ή στη συνάρτηση απ΄όπου καλούνται).


Τέλος εδώ είναι ένα Special μέρος! Έχει προγραμματάκια που έγραψα για να κατανοήσω το πρόβλημα Τόσο καιρό δεν είχα σκεφτεί ότι μπορούσα να μετρήσω το σωρό επιστροφής, και να δω τι κάνει η Visual Basic. Μπορεί να βλέπουμε μια κλήση πχ στην κλήση τμήματος αλλά δεν είναι έτσι ακριβώς μέσα στον διερμηνευτή, υπάρχουν επίπεδα κλήσεων που δεν βλέπουμε (ούτε φανταζόμαστε την χρήση τους).
Στο παρακάτω πρόγραμμα καλούμε την alfa(3) το (x) στον ορισμό θα γραφεί ως Read x (θα αποτελεί την πρώτη γραμμή του εκτελέσιμου κώδικα). Όταν η εντολή Print monitor.stack εκτελεστεί ήδη θα έχουμε μπεί στο Print (μια συνάρτηση εσωτερικά).

function alfa (x) {
      Print monitor.stack
100
      x--
      kappa(x*10)
      if x>1 then 100
      sub delta()
            Print n, monitor.stack
            if x>1 then call alfa(x-1)
           
      end sub
      sub kappa(n)
            delta()
      end sub
}
call alfa(3)

το ίδιο πρόγραμμα με τμήμα
module alfa (x) {
      Print monitor.stack
100
      x--
      kappa(x*10)
      if x>1 then 100
      sub delta()
            Print n, monitor.stack
            if x>1 then call alfa, x-1
           
      end sub
      sub kappa(n)
            delta()
      end sub
}
call alfa, 3

Δείτε τώρα πόσο "κατανάλωση έχει" το ίδιο πρόγραμμα με συναρτήσεις σε παραστάσεις:
Θα δώσω το πρώτο με αφαίρεση του πρώτου Print Monitor.Stack καθώς και στα δύο η κλήση θα γίνει για τον αριθμό 5 (βάλτε και παραπάνω)

function alfa (x) {
            x--
            Print x*10, monitor.stack
            if x>1 then N=Alfa(x-1): N=Alfa(x)
}
N=alfa(5)

Αυτό λοιπόν είναι ισοδύναμο με αυτό:

function alfa (x) {
\\      Print monitor.stack
100
      x--
      kappa(x*10)
      if x>1 then 100
      sub delta()
            Print n, monitor.stack
            if x>1 then call alfa(x-1)
           
      end sub
      sub kappa(n)
            delta()
      end sub
}
call alfa(5)

Από τα παραπάνω παρατηρούμε ότι οι ρουτίνες (sub kappa() και delta()) δεν έχουν καμία επίπτωση στο σωρό επιστροφής (αφού έχουν δικό τους σωρό, ο οποίος μπορεί να μεγαλώνει και για κάθε κλήση το μέγεθος είναι πολύ μικρό).
Επίσης δείτε ότι σε μια ρουτίνα είναι θεατές οι μεταβλητές του τμήματος. Η n όμως φτιάχνεται τοπικά στην Kappa και η delta την βλέπει γιατί στην κλήση από την Kappa το τμήμα έχει και την n. Όταν η Kappa τερματίσει, μετά την κλήση της delta() θα διαγράψει την n. Αν υπήρχε κάποια n στο τμήμα, τότε θα φαίνεται!

Επειδή η Μ2000 έχει νήματα και είναι και καθοδηγούμενη από γεγονότα, σημαίνει ότι όταν τρέξουν τα γεγονότα, θα φτιαχτούν ίσως νέες μεταβλητές, αλλά στην επιστροφή θα καθαρίσουν, και έτσι αυτό που έτρεχε και διέκοψε για να τρέξει το γεγονός, δεν λογαριάζει την "εισβολή". Στα νήματα ισχύει ότι δεν κάνουμε νέες μεταβλητές (μπορούμε όμως), επειδή έχουν το ίδιο όνομα χώρου με το τμήμα που τρέχουν (και το τμήμα μόλις τερματίσει διαγράφει τα νήματα που δημιούργησε). Κάθε νήμα όμως μπορεί να έχει τις δικές του στατικές μεταβλητές που δεν φαίνονται. σε άλλα νήματα. Οι στατικές μεταβλητές είναι εκτός λίστας μεταβλητών, επειδή ανήκουν στο αντικείμενο εκτέλεσης. Κάθε νήμα έχει δικό του αντικείμενο εκτέλεσης και έτσι έχει τις δικές του στατικές μεταβλητές και το δικό του σωρό τιμών. Όταν καλούμε μια ρουτίνα (το νήμα βλέπει τις ρουτίνες του τμήματος), από το νήμα, τότε ο διερμηνευτής δεν διακόπτει την κλήση, αλλά την αφήνει να τερματίσει (εκτός και αν διακοπεί από γεγονός, το οποίο θα πάρει προτεραιότητα).
Τα νήματα δουλεύουν σαν ρολόγια με την διαφορά ότι ο χρόνος εκτέλεσης είναι μέσα στο χρόνο επανάληψης. Δηλαδή βάζουμε κάτι να ξεκινάει κάθε 100ms, και έχει διάρκεια 20 ms, και τα υπόλοιπα 80ms θα τα πάρει το σύστημα. Αν όμως χρειαστεί διάρκεια 120 ms, τότε όχι μόνο θα περάσει κατά 20ms αλλά θα βάλει συν 100ms, (δηλαδή θα χάσει "Φάση").  Αν κορεστεί το σύστημα, δηλαδή αν συμπληρώσει τη μη καταναλωμένη διάρκεια χρόνου, θα μεταθέτει όλα τα νήματα για πιο μετά! Επειδή υπάρχουν δυο πλάνα εκτέλεσης νημάτων, το διαδοχικό και το ταυτόχρονο. η συμπεριφορά που περιέγραψα ανήκει στο διαδοχικό. Το ταυτόχρονο κάνει το εξής, εκτελεί μια εντολή και κοιτάει αν αλλο νήμα έχει συνέχεια!  Εδώ ότι είναι σε μπλοκ εκτελείται χωρίς διακοπή. Στο ταυτόχρονο πλάνο, τα πράγματα είναι περισσότερο πολύπλοκα, και ξεφεύγουν από το σκοπό αυτής εδώ της αναφοράς!