 Ja, herzlich willkommen zurück zu Programmierparadigmen und hier zum zweiten Teil im Modul Composite Types. In diesem zweiten Teil geht es um eine andere Art von Composite Types, nämlich Errays und wie die im Programmiersprachen so repräsentiert werden. Fangen wir mal mit der Frage an, was Errays überhaupt sind. Ich nehme fast an, dass die jeder schon mal irgendwie verwendet hat beim Programmieren, denn sie sind der häufigste zusammengesetzte Datentyp, den man in Programmiersprachen so findet. Konzeptionell ist ein Erray im Prinzip ein Mapping von einem bestimmten Indextypen, zum Beispiel einem Integer, auf einen bestimmten Elementtypen, zum Beispiel Strings oder irgendwelche anderen Daten, die ich gern in dem Erray speichern möchte. Diese Indextypen müssen auf jeden Fall diskrete Typen sein, zum Beispiel Integer, wohingegen die Elementtypen in der Regel jeder beliebige Typ, den es eben in dieser Programmiersprache gibt, sein können. Dieses Konzept von Errays gibt es in quasi jeder Sprache, aber wie das so oft ist, gibt es verschiedene Arten von Sündtags, um dieses Konzept tatsächlich dann zu repräsentieren. Um einfach ein bisschen zu zeigen, was es da für verschiedene Varianten in Programmiersprachen gibt, habe ich hier mal zwei Beispiele, nämlich C und Fortran. Also in C schreibe ich das so, dass ich den Typen der einzelnen Elementte zunächst schreibe, dann den Namen des Errays und anschließend dann die Größe des Errays, also wie viele Elemente von diesem Charaktertypen ich dann in dem Upper Erray haben möchte. Und damit habe ich dann einen Erray von diesem Charaktertype deklariert. In Fortran sieht das Ganze ein bisschen anders aus, denn da kommt auch erst der Elementschip, aber dann zunächst die Größe und anschließend der Name. Und die Größe wird auch nicht in äckigen Klammern, sondern in runden Klammern geschrieben. Obwohl es anders aussieht, meint es aber genau dasselbe, denn das deklariert ganz einfach auch einen Charakter Erray der Größe 26. Neben deklaration kann man natürlich dann die Errays auch benutzen und zum Beispiel auf bestimmte Elemente zugreifen und hier variiert die Sündtags natürlich auch wieder von Sprache zu Sprache. In C greift man auf die Elemente in der Regel mithilfe dieser äckigen Klammern zu, in denen dann der Index steht. Also hier würde ich das Element am Index 3 nehmen und in C beginnen die Indices bei 0. Das heißt, wenn ich hier 3 hinschreibe, ist das das vierte Element in dem Erray. In Fortran sieht es syntaktisch ein bisschen anders aus, aber gar nicht so anders, denn ich benutze einen Runde Klammern, steckt eckigen, ansonsten sieht es eigentlich gleich aus. Aber weil die Indices in Fortran bei 1 beginnen, ist das eben nicht das vierte Element, sondern das dritte. Also man muss eben nicht nur wissen, wie die Sündtags aussieht, sondern auch noch welche Semantik dahinter steckt und in dem Fall eben, wo die Indices tatsächlich beginnen. Neben den Errays, die wir bis jetzt gesehen haben, wo ich im Prinzip eine Sequenz von Werten habe, gibt es natürlich auch mehr dimensionale Errays. Und was hier passiert ist, dass ich einfach mehrere Dimensionen habe, in denen ich Elemente anordnen kann und die ich dann jeweils mit Indices adressieren kann. Also die einfachste Variante haben wir ja schon gesehen, nämlich diese 1-dimensionalen Errays, wo wir einfach nur eine Sequenz von Elementen haben. Wenn ich ein 2-dimensionales Erray habe, dann habe ich im Prinzip ganz einfach dasselbe, bloß eben jetzt als 2D Matrix von Elementen und das funktioniert mit beliebig großen anderen Dimensionen, zum Beispiel eine 3-dimensionales Erray ist im Prinzip einfach eine 3D Matrix von Elementen. Um mal ein konkretes Beispiel zu geben, also in C könnte ich zum Beispiel ein 2-dimensionales Erray so aufschreiben, dass ich hier sage, ich habe da dieses Erray mit 3 rein und 4 spalten und zwar so, dass jedes Elementerin vom Typ Ind ist und das definiert im Prinzip so eine Tabelle, so eine 3 mal 4 Tabelle, die dann auch entsprechend so im Speicher repräsentiert wird. Wenn ich jetzt so ein Erray habe, dann kann ich damit in verschiedenen Sprachen eine ganze Reihe von Dingen tun und zwar nicht nur eben auf einzelne Elemente zugreifen und diese einzelnen Elemente lesen und schreiben, sondern eben auch noch deutlich mehr. Was genau hängt von der Sprache ab? Es gibt so 3 Arten von Operationen, die man relativ häufig findet. Und zwar ist das erste von der sogenannte Slicing, was im Prinzip bedeutet, dass ich aus dem Erray ein Teil rausschneide, der in der Regel so einem Rechteck entspricht, wenn ich mir das jetzt mal auf Papier oder so aufmalen würde, wie das Ganze im Speicher repräsentiert wird. Wie genau das funktioniert, hängt sehr von der Sprache ab und wir werden gleich ein Beispiel sehen von einer Sprache, die sehr reiche Slicing-Operatoren für Arrays anbietet. Eine andere Art von Operationen, die häufig auf Arrays angewandt werden, sind Vergleiche. Und zwar möchte man manchmal vielleicht nicht nur einzelne Elemente im Erray vergleichen, sondern wirklich alle Elemente miteinander vergleichen. Also wenn ich zum Beispiel 2 Arrays, Array1 und Array2 habe, die die gleiche Länge haben, dann kann ich in manchen Sprachen mit so einem Operator hier die Elemente elementweise vergleichen, also das erste mit dem ersten, das zweite mit dem zweiten usw. Und bekomme dann ein Array als Ergebnis raus, was das Ergebnis der elementweisen Vergleiche an den jeweiligen Elementen hat und dann natürlich die gleiche Länge, wie Array1 und Array2 auch wieder hat. Eine andere Gruppe von Operationen sind alle möglichen mathematischen Operationen, also zum Beispiel elementweise Additionen oder Subtraktionen, bei denen so ähnlich wie bei dem Vergleich hier oben einfach 2 Arrays derselben Länge erwartet werden und dann als Ergebnis auch wieder ein Array genau dieser Länge rauskommt. Als konkretes Beispiel schauen wir uns mal Array Slicing Operation in Fortran an. Der Grund, warum wir uns Fortran hier anschauen, ist, dass das eine Sprache ist, die schon sehr früh eine sehr mächtige Menge von diesen Slicing-Operatoren in der Sprache direkt definiert hatte und ist einfach ganz interessant zu sehen, was man damit so alles machen kann. Es gibt in manchen anderen Sprachen manchmal dann auch mit Hilfe von Bibliotheken ähnliche Operationen, die man dann auch verwenden kann. Zum Beispiel gibt es in Python die Möglichkeit, Arrays mit Hilfe von zum Beispiel NumPy oder anderen Bibliotheken ähnlich zu Slicing, aber was wir jetzt hier anschauen, sind Beispiele, die direkt in Fortran mit Hilfe der Fortran-Sprache möglich sind. Für diese 3 Beispiele, die wir gleich sehen werden, gehen wir davon aus, dass wir einen 10 mal 10 Array haben, welchen Typ die Elemente haben, ist im Prinzip egal und der Name dieses Arrays ist Matrix. Schauen wir uns mal das erste Beispiel an. Also ich mal da jeweils mal dieses 10 mal 10 Array auf. Oder versuche es zumindest. Wunderschön. Und wenn ich jetzt aus diesem 10 mal 10 Array ein, sozusagen recht heraus schneiden möchte, dann ist ein Weg, wie ich das machen kann, indem ich den Namen des Arrays benutze, also Matrix, und dann einfach zwei Ranges angeben, zwei Bereiche mit jeweils einem Start und einem Endindex in der ersten bzw. in der zweiten Dimension. Also wenn ich jetzt zum Beispiel, sag mal mal, die Werte haben möchte, die ich jetzt hier blau markiere, alles von hier bis hier und dann sag mal mal nach unten noch all das hier, und zwar dieses komplette Rechteck, dann könnte ich das ganz einfach aufschreiben, indem ich jetzt sage, ich möchte in der ersten Dimension alles vom Index 3 bis 6 rausschneiden aus diesem Array und in der zweiten, in der y Dimension, alles vom Index 4 bis 7. Und das sollte dann eben genau diese blauen Elemente ergeben. Noch mal zur Erinnerung, die Indices beginnen in Fortran by 1, das heißt, das dritte Element, also hier wäre 1, 2, 3, in der x Dimension ist dann tatsächlich dieses dritte, wo wir hier beginnen, rauszuschneiden. Schauen wir mal noch ein zweites Beispiel an und auch dafür versuche ich mal wieder dieses 10 x 10 Gerüst aufzumalen. Und in diesem zweiten Beispiel gehen wir mal davon aus, dass ich vielleicht alle Elemente innerhalb einer bestimmten Reihe haben will, beginnend ab einem bestimmten Element, also sag mal mal zum Beispiel, ich möchte alles ab dem sechsten Element in der ersten Dimension haben, und zwar die fünfte Reihe, also sozusagen 2, 3, 4, 5 sozusagen alles ab hier und dann bis zum Ende, egal wie weit nach rechts das Ganze jetzt noch geht, dann könnte ich das so aufschreiben, dass ich wieder den Namen dieses Arrays benutze, dann sage ich in der ersten Dimension, wo es losgeht und auch wieder den Doppelpunkt, lass aber das Ende dieses Ranges, dieses Bereich jetzt weg, was so viel bedeutet, wie soweit es ihnen geht. Und in der zweiten Dimension gebe ich keinen Bereich an, sondern nur einen einzelnen Wert, nämlich den fünften und dann käme eben genau dieses blaue Array, was ich hier markiert habe, raus. Das dritte Beispiel zeigt mal, dass wir eben nicht nur einzelne Rechtecke aus diesem Array rausschneiden können, sondern auch mehrere Rechtecke, sofern sie denn in irgendeinem bestimmten Muster angeordnet sind. Dafür auch mal wieder unser 10 x 10 Array. Und für das Beispiel gehen wir mal davon aus, dass wir vielleicht alle Elemente bis zum vierten Element in der ersten Dimension haben wollen, aber nicht alle Reihen in dieser Matrix, sondern sagen wir mal nur die zweite, vierte, sechste und achte. Und zum Beispiel dann das, das und das, also bis zum vierten, bis zum Index vier in der ersten Dimension, aber nur die zweite Reihe. Und dann lassen wir die dritte weg und wollen das Ganze auch nochmal für die vierte Reihe, für die sechste Reihe und für die achte Reihe. Und auch das ist was, was man mithilft für diese Array-Slicing-Notation, die vortran auf Sprach-Ebene anbietet, sehr einfach machen kann. Ich würde nämlich in dem Fall sagen, gib mir alles bis zum Index vier in der ersten Dimension. Ich lasse den Anfang des Ranges weg und sage einfach bis vier. Und um dieses jede zweite Reihe, beginnt ab zwei bis acht auszudrücken, würde man das so aufschreiben, wobei die erste zwei bedeutet, dass es in Reihe zwei losgeht, die achte bedeutet, dass es bis Reihe acht geht. Und die zwei am Ende bedeutet, dass wir das immer in Zweierschritten machen, also sozusagen jede zweite Reihe weglassen. Und was wir dann bekommen, ist ein weiteres Array, was eben genau diese blauen Felder enthält. So ähnlich wie bei den Records oder im Prinzip bei allen Composite Types ist es natürlich sehr wichtig zu verstehen, wie diese Typen denn tatsächlich im Speicher repräsentiert werden. Für ein Dimensionale Arrays ist es im Prinzip sehr einfach. Das wird eigentlich in allen Sprachen so gemacht, dass die Elemente nacheinander im Speicher stehen, und zwar einfach in der Reihenfolge, in der sie tatsächlich auch in dem Array drin sind. Etwas interessanter wird es dann bei mehrdimensionalen Arrays, denn hier gibt es verschiedene Optionen, wie man das Ganze umsetzen kann. Und in den üblichen Sprachen gibt es im Prinzip drei Antworten auf die Frage, wieso ein mehrdimensionales Array denn im Speicher abgebildet wird. Die erste Option, das wird zum Beispiel in C gemacht, ist, dass wir alle Elemente dieses Arrays nacheinander haben, also contiguous, aber das so machen, dass die Rows, also die Reihen zunächst, ja, die dominierenden Dimensionen sind und ich sozusagen erst die komplette erste Reihe habe, und dann die komplette zweite Reihe und so weiter. Option zwei ist so ähnlich, aber irgendwie auch ein bisschen anders. Das wird zum Beispiel in Fortran gemacht. Auch hier werden alle Elemente nacheinander direkt im Speicher abgebildet, aber die dominierende Dimension ist eben nicht die der Reihe, sondern die der Spalte. Das heißt, wir haben zuerst alle Werte der ersten Spalte, dann alle Werte der zweiten Spalte und so weiter. Das heißt, es kommt ein bisschen was anderes raus als in der Option eins. Die dritte Option nennt sich Row Pointer Layout. Das wird zum Beispiel in Java verwendet. Und hier ist die Idee, dass die Werte nicht unbedingt direkt nacheinander alle im Speicher stehen, sondern dass für jede Reihe das Arrays ein Pointer existiert, der dann auf ein Stück Speicher zeigt, was vielleicht nacheinander angeordnet ist, aber vielleicht auch ganz woanders sein kann. Und somit eben nicht sichergestellt ist, dass die Elemente wirklich nacheinander im Speicher zu finden sind. Schauen wir uns diese drei Varianten mal mithilfe von einem Beispiel an. Und zwar haben wir in dem Beispiel ein Int Array, was ich jetzt hier mal so in C-artiger Notation aufschreibe. Und dieses Int Array hat zwei Reihen und vier Spalten. Und wir initialisieren das auch gleich, so dass die Werte alle feststehen. Und zwar schreiben wir 1, 2, 3, 4 in die erste Reihe und 5, 6, 7, 8 in die zweite Reihe. Und die Frage ist jetzt, wie wird das Ganze im Speicher repräsentiert mithilfe dieser drei Varianten, die wir ja gerade schon gesehen haben. Fangen wir mal mit der ersten an, also mit der Row Major Variante, die sich zum Beispiel in C wiederfindet. Und für jede dieser Varianten werde ich einfach den Speicher mal so aufschreiben oder aufmalen, wie er denn dann tatsächlich aussieht. Und zwar jeweils indem ich hier so einen langen Block schreibe, also das stellt einfach mal den ganzen Speicher dar. Und zwar so, dass nach rechts die größeren Adressen kommen und irgendwo innerhalb dieses Speichers geht das Array dann los. Und im Falle von einem Row Major Layout werden tatsächlich erst die komplette erste Reihe und dann die komplette zweite Reihe reingeschrieben. Das heißt, ich habe irgendwo hier den Wert 1, dann den Wert 2, dann den Wert 3, dann den Wert 4. Dann sind wir fertig mit der ersten Reihe und dann kommt die zweite Reihe. Und der Beginn des Arrays beziehungsweise da, wo dann auch der Pointer, an dem das Array gespeichertes hinzeigen würde, ist eben genau hier. Soweit so gut, das ist vielleicht das Layout, was man sich jetzt intuitiv auch als erstes vorgestellt hätte. Ein bisschen anders sieht es aus, in Sprachen die Color Major Layout verwenden. Denn hier erscheinen die Zahlen eben nicht in dieser schönen Reihenfolge, die wir gerade gesehen haben, sondern das Ganze sieht ein bisschen anders aus. Also ich habe hier wieder mein Speicher, irgendwo geht das Array dann los. Und weil wir hier jetzt Color Major Layout verwenden, erscheinen zuerst 1 gefolgt von 5, also sozusagen die Werte der ersten Spalte in unserer 2D Matrix. Dann die Werte der zweiten Spalte, also 2 und 6. Dann die Werte der dritten Spalte, also 3 und 7. Und anschließend die Werte der vierten und letzten Spalte, nämlich 4 und 8. Das heißt, das Array ist genauso groß wie in der Repräsentation oben drüber, aber die Reihenfolge der Elemente ist eben eine andere. Die dritte Variante, die zum Beispiel dann in Java zum Tragen kommt, war die Row Pointers Repräsentation. Und die sieht wie folgt aus, also ich habe wieder irgendwo mein Speicher. Ich habe wieder irgendwo die Stelle, wo das Array gespeichert ist. Und habe jetzt für jede Reihe innerhalb meines Arrays einen Pointer, der irgendwo hin zeigt, wo dann tatsächlich diese Reihe gespeichert ist. Und das kann ganz woanders im Speicher sein, vielleicht auch viel weiter hinten. Also hier wird das Beispiel mal davon aus, dass zum Beispiel die zweite Reihe hier gespeichert ist, wo ich dann 5, 6, 7 und 8 im Speicher habe. Das heißt, für meinen Pointer für die zweite Reihe muss dann sozusagen hierhin sein. Und irgendwo anders im Speicher habe ich vielleicht die erste Reihe stehen, zum Beispiel hier hinten, wo dann 1, 2, 3, 4 drin steht. Und der Pointer dahin zeigt dann eben zum Beginn dieser Reihe. Und das Wichtige hieran ist, dass die Reihen irgendwo im Speicher sein können. Sie können tatsächlich auch so aussehen wie im Row Major Layered, aber sie müssen eben nicht. Sondern das Array selbst besteht erstmal nur aus Pointers. Diese Pointer zeigen dann dahin, wo die einzelnen rein stehen und da befinden sich dann die eigentlichen Daten. Warum ist es denn nicht so wichtig, in welche Reihenfolge diese Array-Elemente dann überhaupt im Speicher abgebildet werden? Na ja, die Antwort ist wie so oft, es geht um Effizienz. Denn das Speicher-Layout bestimmt, wie effizient es nun ist, durch so ein multidimensionales Array durchzuiterieren. Zum Beispiel, indem ich verschachtelte Schleifen habe, die dann durch die verschiedenen Dimensionen durchgehen. Der Grund, warum das Memory-Layout so eine große Rolle spielt, ist ganz einfach, wie die CPU tatsächlich auf den Speicher zugreift. Und zwar passiert das, wie Sie vielleicht schon in irgendeiner anderen Veranstaltung mal gehört haben, nicht so, dass wirklich jedes einzelne Element da immer einzeln gelesen wird, sondern die CPU liest komplette Cashlines aus dem Speicher. Das heißt, anstatt jetzt nur einen Element zu lesen, wird eben eine komplette, ja, eine größere Menge von Speicher mit einmal gelesen. Und es ist sehr effizient, wenn man all die Daten, die in so einer Cashline drin sind, dann tatsächlich auch liest. Wohingegen es sehr ineffizient ist, so wild durch den Speicher zu springen und mal ein bisschen was von hier und mal ein bisschen was von da zu lesen oder auch zu schreiben. Denn das führt dann zu sogenannten Cashmisses. Das heißt, wir werden verschiedene Cashlines immer wieder in den Speicher laden und eben nicht die, die wir gerade schon geladen haben, wieder verwenden. So, um das Ganze noch ein bisschen zu vertiefen, habe ich jetzt wieder ein kleines Quiz, bei dem es eben genau um Memory Layouts und die effiziente Art und Weise durch so ein Array dann durchzugehen geht. Und zwar haben wir hier ein zweidimensionales Array und gehen davon aus, dass dieses Array entweder in C oder in Fortran benutzt wird. Noch mal zur Erinnerung, C hat Row Major Layout, Fortran hat Column Major Layout. Und die Frage ist jetzt, welche dieser vier Varianten durch das Array durchzugehen, zwei sind in C, zwei sind in Fortran, sind denn jetzt eigentlich die schnellsten in dieser jeweiligen Sprache, wenn man sich überlegt, wie dieses Array nun tatsächlich im Speicher abgelegt ist. Ich würde Sie bitten, da jetzt mal drüber nachzudenken, vielleicht auch nochmal aufzumalen, wie das Array tatsächlich im Speicher aussieht, dann im Ilias abzustimmen und anschließend erkläre ich dann, was jetzt die Lösung ist. Okay, schauen wir uns mal an, was die schnellsten Optionen hier sind. Also für den C Code ist die erste Option tatsächlich die schnellste und zwar aus dem Grund, dass die Schleifen hier so verschachtelt sind, dass wir eben genau in der Reihenfolge durch den Speicher gehen, in der die Elemente auch im Speicher repräsentiert sind. Das heißt, wir bekommen quasi den größten Benefit aus den Cashlines, die wir eh schon lesen und müssen dadurch nicht mehrmals auf dieselben Cashlines zugreifen, sondern benutzen die eine Cashline vollständig und gehen dann zur nächsten Cashline. Das Ganze passiert so, weil wir eben die erste Dimension in der äußeren Schleife haben, also das I hier steht und die innere Dimension dann in der inneren Schleife. Für Fortran ist eben genau der andere Code effizient, weil Fortran eben Colour Major Layout benutzt, das heißt, hier ist die schnellste Variante tatsächlich in der äußeren Schleife die Variable J zu inkrementieren und nur in der inneren Schleife die Variable I, das heißt, wir gehen quasi erst durch eine Column komplett durch und gehen dann zur nächsten Column und gehen die wieder komplett durch. Und das heißt, dass wir eben, weil Fortran Column Major Layout verwendet, tatsächlich im Speicher genau so durch die Elemente gehen, wie sie auch im Speicher abgelegt sind. Ja, damit werden wir auch schon wieder am Ende vom zweiten Teil dieses Moduls zum Thema Composite Types. Ich hoffe, Sie wissen jetzt ein bisschen mehr über Ares, welche Operations denn eigentlich auf Ares gibt und wie Ares eigentlich im Speicher abgelegt werden und warum das auch eine Rolle spielt, nämlich, um effizient mit diesen Ares arbeiten zu können. Damit danke fürs Zuhören und dann bis zum nächsten Mal.