Automatisiertes rekursives Computing

1. Einleitung


Der Einfluss von Subroutinen (englische Subroutine) auf die Programmierung ohne Übertreibung ist enorm. Sie wurden zu Beginn der Programmierung eingeführt und verlieren bis heute nicht ihre Relevanz. Ohne sie ist eine praktische Programmierung einfach nicht vorstellbar. Obwohl aus formaler Sicht, sind sie nicht so notwendig, weil Die reine Theorie interessiert sich mehr für die Eigenschaften des Algorithmus als für seine Größe.

In der Automatentheorie wird das Konzept verschachtelter Automaten, auf dessen Grundlage die Praxis von Automatenunterprogrammen (AMP) basieren würde, selten diskutiert. Eine ähnliche (verschachtelte) hierarchische Organisation von Automaten ist, wenn sie berücksichtigt wird, sehr oberflächlich. Einer der Gründe für diese Beziehung kann die Komplexität der Implementierung einer verschachtelten Hierarchie auf Hardwareebene sein [1, 2].

Die Programmierung ist flexibler und bietet mehr Funktionen als das Entwerfen digitaler Schaltungen. Wir müssen dies sicherstellen, indem wir die Software-Implementierung verschachtelter Automaten und dann das Konzept der automatischen rekursiven Algorithmen weiter berücksichtigen.

Bei allen besonderen Problemen bei der Bildung eines verschachtelten Automatenmodells verursacht seine formale Definition keine Probleme. Andererseits wird die Wahl des Aufbaus einer Modellhierarchie sicherlich erhebliche Auswirkungen auf die Softwareimplementierung haben.

2. Automatisierte rekursive Algorithmen


Im vorherigen Artikel [3] wurde nur eine formale Definition des Steuerungsmodells für automatische Programme (AP) gegeben, ohne dessen Implementierung und spezifische Anwendungsbeispiele zu berücksichtigen. Die Möglichkeit der Implementierung rekursiver Algorithmen wurde ebenfalls erwähnt. Als nächstes betrachten wir anhand des Beispiels für die faktorielle Berechnung zunächst die Softwareimplementierung der Mechanismen zum Erstellen automatischer Routinen im Rahmen des C ++ - Paradigmas des Objektautomaten, und zweitens definieren wir, da wir einen rekursiven Algorithmus betrachten, im Wesentlichen ein allgemeines Prinzip für die Implementierung eines solchen Algorithmen im Rahmen der automatischen Programmierung.

Die Verwendung rekursiver Funktionen in einer API kann recht einfach sein. Dies zeigt den Code zum Auflisten von 1 des Objektmaschinenprogramms. Hier ist die FSimpleFactorial-Automatenklasse kein Prozess, sondern eine Automatenroutine, weil enthält den Endzustand „00“ (weitere Details zu Unterprogrammen siehe [3]). Auf der Verhaltensebene entspricht das erzeugte Automatenobjekt einem Automaten mit einem bedingungslosen Übergang vom Ausgangszustand „f0“ in den Endzustand „00“ und einem Aufruf dieses Übergangs im Rahmen der Aktion y1 der üblichen faktoriellen Fakultätsfunktion (Fakultät (...)).

Listing 1. Aufruf der Fakultätsfunktion vom AMS
#include "lfsaappl.h"

class FSimpleFactorial : public LFsaAppl
{
public:
    double dF{1};		//	 
    FSimpleFactorial(int n);
    virtual ~FSimpleFactorial() {};
private:
    int nN{0};
    void y1();
    double factorial(int n);
};

#include "stdafx.h"
#include "FSimpleFactorial.h"

LArc TT_SimpleFactorial[] = {
    LArc("f0", "00", "--",	"y1"),		//
    LArc()
};
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
FSimpleFactorial::FSimpleFactorial(int n)
    :LFsaAppl(TT_SimpleFactorial, "FSimpleFactorial")
{
    nN = n;
}
//
void FSimpleFactorial::y1() { dF = factorial(nN); }

double FSimpleFactorial::factorial(int n)
{
    if (n == 0) return 1;
    else return n*factorial(n-1);
}


Da das FSimpleFactorial-Objekt ein verschachtelter Automat (Unterprogramm) ist, muss es einen „Wrapper“ dafür geben - den Prozess, der es aufruft. Sein Beispiel ist ein Prozess, der aus einem Objekt namens FTskSimple erzeugt wurde, dessen Code in Listing 2 gezeigt wird.

Listing 2. Der Vorgang zum Erstellen eines verschachtelten Automaten
#include "lfsaappl.h"

class FTskSimple : public LFsaAppl
{
public:
    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FTskSimple(nameFsa); }
    bool FCreationOfLinksForVariables();
    FTskSimple(string strNam);
    virtual ~FTskSimple();

    CVar    *pVarF;
    CVar    *pVarN;
    CVar    *pVarStart;

    LFsaAppl *pFCall{nullptr};
protected:
    int x1();
    void y1(); void y2(); void y3();
};

#include "stdafx.h"
#include "FTskSimple.h"
#include "FSimpleFactorial.h"
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
LArc TT_Task[] = {
    LArc("st", "t1", "--",	"y1"),	//
    LArc("t1", "t2", "x1",	"y3"),	//
    LArc("t2", "t1", "--",	"y2"),	//
    };

FTskSimple::FTskSimple(string strNam)
    :LFsaAppl(TT_Task, strNam)
{
}

FTskSimple::~FTskSimple() { if (pFCall) delete pFCall; }

bool FTskSimple::FCreationOfLinksForVariables() {
    pVarF = CreateLocVar("dF", CLocVar::vtDouble, "factorial value");
    pVarN = CreateLocVar("nN", CLocVar::vtInteger, "input value");
    pVarStart = CreateLocVar("bStart", CLocVar::vtBool, "start?");
    return true;
}

int FTskSimple::x1() { return pVarStart->GetDataSrc(); }

void FTskSimple::y1()
{
    // reset flag start
    pVarStart->SetDataSrc(this, 0.0);
    // creating and calling a subroutine
    pFCall = new FSimpleFactorial(pVarN->GetDataSrc());
}
void FTskSimple::y2()
{
    // display factorial value
    pVarF->SetDataSrc(this, static_cast<FSimpleFactorial*>(pFCall)->dF);
}

void FTskSimple::y3()
{
    // reset flag start
    pVarStart->SetDataSrc(this, 0.0);
    static_cast<FSimpleFactorial*>(pFCall)->nN = pVarN->GetDataSrc();
    // creating and calling a subroutine
    pFCall->FCall(this);
}


Dieser Vorgang beim Übergang vom Ausgangszustand "st" zum Zustand "t1" erzeugt ein Objekt vom Typ FSimpleFactorial, das die Grundlage für die Erstellung eines verschachtelten Automaten bildet (im üblichen Sinne Aufruf eines Unterprogramms). Basierend auf dem aktuellen Status der lokalen Variablen vom booleschen Typ bStart (siehe auch den Prädikatcode x1) bewirkt dies einen Übergang vom Status "t1" zu "t2". Bei diesem Übergang setzt die Aktion von y1 erstens den Wert dieser lokalen Variablen zurück (um einen falschen Neustart zu verhindern) und ruft zweitens die FCall-Prozedur auf, die einen verschachtelten Automaten erstellt.

Der Interpreter von Automaten der VKPa-Umgebung organisiert den Übergang in den Ausgangszustand „f0“ des verschachtelten Automaten und dessen Zustände werden zu den aktuellen Zuständen des Prozesses. Der Übergang des verschachtelten Automaten in den Endzustand "00" führt die Berechnung auf die vorherige Verschachtelungsebene und in unserem Fall auf den Abschluss des Übergangs des Automaten der obersten Ebene in den Zustand t2 zurück. Dann führt der Prozess bei einem bedingungslosen Übergang vom Zustand "t2" zum Zustand "t1" die Aktion y2 aus, die das Ergebnis der Berechnung der Fakultät der lokalen Variablen des Prozesses dF festlegt.

Beachten Sie, und es ist sehr wichtig, dass das Ergebnis eines verschachtelten Automaten erst nach Abschluss seiner Arbeit und dem Übergang zum nächsten Status eines Automaten der obersten Ebene verfügbar ist. Es wird möglich sein, es (Ergebnis) durch einen Zeiger auf ein Objekt eines verschachtelten Automaten zu erhalten.

Und ohne die Laufzeit der rekursiven Funktion wäre alles in Ordnung. Im Vergleich zur üblichen Unterroutine ist sie aufgrund der Verschachtelung in der Regel viel größer und kann daher die bedingte Atomizität der Aktionen automatischer Prozesse verletzen, deren Gesamtausführungszeit in den Wert eines diskreten Taktzyklus passen sollte.

In unserem Fall ist die Berechnung der Fakultät recht klein und bis zum Maximalwert eines Doppelergebnisses für n = 170 passt in 1 Mikrosekunde. Um große Werte zu berechnen, muss auf lange Arithmetik umgeschaltet werden (siehe zum Beispiel [4, 5]). Gleichzeitig wird die faktorielle Rechenzeit noch weiter zunehmen und fast garantiert über den diskreten Taktzyklus hinausgehen, was sich auf die Geschwindigkeit der verbleibenden Netzwerkmaschinen auswirkt, die im nicht präemptiven Multitasking-Modus arbeiten, und auf die Reaktivität der Reaktion der gesamten Anwendung, die sich in ihren „Bremsen“ manifestiert.

Sie können innerhalb eines diskreten Taktzyklus bleiben und den „Bremsen“ -Defekt beseitigen, indem Sie die Berechnung in Schritte erweitern. Für diese Zwecke ist es zweckmäßig, den üblichen faktoriellen Algorithmus in eine automatische Form zu übersetzen. Aufgrund des Interpretationsmodus von Automaten erhöht sich zwar die Berechnungszeit, aber die zeitlichen Eigenschaften der Anwendung werden wieder normal.

Listing 3 zeigt die Berechnung der Fakultät in ihrer üblichen Form und in der Form, die für die spätere Umwandlung in eine Automatenform vorbereitet wurde. Letzteres stellt explizit atomare Schritte dar, die im Quellcode des Programms verborgen sind. In diesem Fall handelt es sich um Operatoren der Form y = f (...), wobei f eine gewöhnliche oder rekursive Programmfunktion ist. Ein solcher Datensatz maskiert die Betriebszeit der Funktion und erzeugt einen falschen Eindruck von der "sofortigen" Zuweisung des Werts der Variablen y. In Wirklichkeit gibt es weder das eine noch das andere.

Listing 3. Softwarefunktionen zur Berechnung der Fakultät
int factorial(int n)
{
    if (n == 0) return 1;
    else return n*factorial(n-1);
}

double Factorial(int n)
{
    double dF = 1;
    if (n == 0)
        return dF;
    else {
        double dPrev = Factorial(n-1);
        double dF = n*dPrev;
        return dF;
    }
}


Im Rahmen des Konzepts der automatischen Programmierung werden atomare Aktionen (Methoden) von automatischen Softwareobjekten als bedingt augenblicklich betrachtet. Und wenn nur die Gesamtbetriebszeit von Aktionen in einen einzelnen Zyklus der diskreten Zeit des Automatenraums passt, entspricht die Softwareimplementierung ihrem formalen Modell.

Bemerkung 1. Die Gesamtbetriebszeit von Aktionen in dem einen oder anderen diskreten Taktzyklus kann sehr stark variieren. Im Falle eines Überschreitens der Zykluszeit versucht die VKPA-Umgebung, dies bei nachfolgenden Zyklen des Automatenraums zu kompensieren, wodurch die diskrete Zeit nachfolgender Zyklen nach Möglichkeit verringert wird.

In Abb. Abbildung 1 zeigt ein Blockdiagramm mit einem Markup, das die Zustände einer äquivalenten endlichen Zustandsmaschine und die Namen ihrer Prädikate und Aktionen widerspiegelt. Das Automatenmodell selbst in Form eines Diagramms des Automaten ist in Abb. 1 dargestellt. 2.

Bild
Abb. 1. Blockschaltbild des rekursiven Algorithmus faktoriell

Bild
Abb. 2. Ein Automatenmodell eines rekursiven faktoriellen Algorithmus

Der Code für die automatische Routine zur Berechnung der Fakultät ist in Listing 4 dargestellt. Die Kommentare in der Übergangstabelle (TP) geben die Zeit für die Berechnung der Fakultät in ms und die Anzahl der für das Berechnungsverfahren für n = 170 aufgewendeten diskreten Schritte wieder. Wenn die Berechnungszeit von der Geschwindigkeit der Computerplattform und / oder von der Art des Projekts (Debug / Release) abhängt, wird die Anzahl der diskreten Schritte (Taktzyklen) nur durch die Eigenschaften des Algorithmus bestimmt und kann unabhängig von der Form seiner Darstellung und Implementierung als objektive Bewertung der Geschwindigkeit des Algorithmus dienen.

Im Fall eines Automatenmodells kann die Anzahl der diskreten Ticks ganz einfach berechnet werden, und nachdem bestimmte Statistiken gesammelt wurden, kann eine analytische Formel für die Geschwindigkeit des Algorithmus abgeleitet werden. In der Zwischenzeit sind solche Schätzungen sehr ungefähr und basieren auf Schlussfolgerungen, die häufig aufgrund recht komplexer Überlegungen gewonnen wurden.

Listing 4. Automatisierte faktorielle Routine
#include "lfsaappl.h"

class FFactRec : public LFsaAppl
{
public:
    FFactRec(int n);
    virtual ~FFactRec() { if (pFFactRec) delete pFFactRec; };
    int nN;		//	n
    double dF;	//	n!
private:
	int x1();
    void y1(); void y2(); void y3();
protected:
    void CallFactorial();
    double  PrevFactorial();
    FFactRec *pFFactRec{nullptr};
};

#include "stdafx.h"
#include "FFactRec.h"

#define QT_NO_DEBUG_OUTPUT

extern LArc TT_FactRec[];
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
//	   
FFactRec::FFactRec(int n)
    :LFsaAppl(TT_FactRec, "FFactRec")
{
    nN=n; dF=1;
}

LArc TT_FactRec[] = {
//        Debug    Release     Steps
//msec: 120-129   98-101        340
    LArc("f1", "00", "x1",	"y3"),	//
    LArc("f1", "f2", "^x1",	"y1"),	//
    LArc("f2", "00", "--",	"y2"),	//
    LArc()
};

int FFactRec::x1() { return nN==0; }
//   
void FFactRec::y1() { CallFactorial(); }
//	1.      
//	2.    
void FFactRec::y2() { dF = PrevFactorial()*nN; }
// 0!
void FFactRec::y3() { dF = 1; }
//   (  )
void FFactRec::CallFactorial()	{
    //	     
    pFFactRec = new FFactRec(nN-1);
    pFFactRec->FCall(this);
};
//    
double  FFactRec::PrevFactorial()	{ return pFFactRec->dF; };


3. Schlussfolgerungen


Ich gebe zu, dass ich aufgrund der Besonderheiten der zu lösenden Aufgaben keine rekursiven Algorithmen in realen Projekten verwenden und entwickeln musste, außer zu Schulungszwecken und zum Testen des Kernels für die Implementierung der Verschachtelung. Automatisierte Routinen - fast ständig und in großer Anzahl. Unter diesen sind die als Trägheit bezeichneten Unterprogramme besonders interessant (für weitere Einzelheiten siehe [3]). Aber sie sollen ihrer Betrachtung einen eigenen Artikel widmen.

Rekursive Algorithmen sind ein wichtiger Bestandteil der Programmtheorie [6]. Ohne die Implementierung von Routinen und Rekursionen ist es aus allen Blickwinkeln schwierig, sich ein effizientes und bequemes Rechenmodell vorzustellen. Und angesichts der Möglichkeiten der Objektprogrammierung (siehe Beispiele des Artikels) ist es nicht so schwierig, eine Rekursion zu implementieren, nachdem ein Modell von Unterprogrammen erstellt wurde. Sie können natürlich bestimmte Methoden zum Konvertieren der Rekursion in eine reguläre Form verwenden und diese mit denselben Zyklen umgehen. Es ist jedoch vorzuziehen, es nicht dumm loszuwerden, sondern direkte Mechanismen für seine Umsetzung zu haben. Natürlich wird es langsamer, natürlich teurer in Bezug auf Ressourcen (Kosten für denselben Stapelspeicher), aber mit den vorhandenen Hardwarefunktionen wird dies nicht so kritisch sein.

Rekursive Algorithmen sind aber auch als Tests zur Bewertung der Fähigkeiten, Universalität und Effektivität algorithmischer Modelle interessant. In diesem Sinne implementiert das Automatenmodell nicht nur die Rekursion sehr effektiv, sondern fügt als Bonus auch „Automatenchips“ hinzu - die Fähigkeit, den internen Zustand des rekursiven Algorithmus zu steuern, Schritte zur Bewertung der Leistung zu zählen, die Ausführungszeit des Algorithmus zu steuern usw. usw.

Rekursion zu bekämpfen ist sinnlos genug. Es muss verwendbar sein. Ich zitiere, dem ich voll und ganz zustimme: „Auf den ersten Blick mag die Rekursion kompliziert erscheinen. In einigen Fällen ist die rekursive Methode jedoch unglaublich effektiv, wenn alles richtig gemacht wird. Manchmal ist es jedoch besser, Schleifen zu verwenden. Das Verständnis beider Methoden und die Fähigkeit, sie effektiv einzusetzen, hilft Ihnen bei Ihrer Arbeit und ist im Interview von Vorteil. “[7] Ich kann nur alleine hinzufügen, dass automatische Modelle rekursiver Algorithmen es ermöglichen, mithilfe der "automatischen Eigenschaften" eines Rechenmodells die Rekursion zu verstehen, zu debuggen und einen solchen Algorithmus viel schneller zu verfeinern.

Und der Letzte. Trotzdem möchte ich eine Antwort auf die Frage bekommen - wie läuft es mit der Coroutine-Rekursion? Ich habe es bereits gefragt, aber ich habe keine Antwort erhalten ... Schließlich ist es eine Sache, eine Million Coroutine zu erstellen (siehe [8] für Beispiele) und eine andere, einen rekursiven Algorithmus zu implementieren, dessen Verschachtelungsebene nicht gleich, aber hoch genug ist. Und anscheinend interessiert die Antwort auf diese Frage nicht nur mich allein ... [9]

Literatur
1. .., .. . I, . ., 1981, 2, 135-144. [ ], : mi.mathnet.ru/at5725 . . . ( 10.03.2020).
2. .., .. . II, . ., 1981, 3, 112-121. [ ], : mi.mathnet.ru/at5743 . . . ( 10.03.2020).
3. . [ ], : habr.com/ru/post/484588 . . . ( 10.03.2020).
4. . [ ], : habr.com/ru/post/255761 . . . ( 10.03.2020).
5. C++. [ ], : habr.com/ru/post/172285 . . . ( 10.03.2020).
6. . . : . . – .: , 1983. – 256 .
7. , ? Python. Ethan Jarrell. Recursion vs. Looping in Python [ ], : nuancesprog.ru/p/3325 . . . ( 15.03.2020).
8. Your first coroutine with Kotlin. [ ], : kotlinlang.org/docs/tutorials/coroutines/coroutines-basic-jvm.html . . . ( 18.03.2020).
9. . CyberForum.ru. [ ], : www.cyberforum.ru/unity/thread2479923.html . . . ( 18.03.2020).

All Articles