Höllische Programmiersprachen Seminar im Wintersemester 2014

Werbung
Höllische Programmiersprachen
Seminar im Wintersemester 2014/15
API als Syntax vs. Bibliotheken
Markus Siglreithmaier
Technische Universität München
Zusammenfassung
Diese Ausarbeitung behandelt zwei Implementierungskonzepte der
API in unterschiedlichen Programmiersprachen. Hierbei werden die beiden Designkonzepte, das Interface als Teil der Sprachsyntax gegenüber
der Auslagerung in eine Bibliothek genauer erläutert. Dazu nach einem
kurzen Überblick der verschiedenen Methodiken ein Vergleich hinsichtlich
Performanz, Usability und Komplexität der Implementierung.
1
Einführung
Eine wichtige Komponente und Qualitätsfaktor einer Sprache stellt die Programmierschnittstelle dar. Daher ergibt sich, dass deren Design und Implementierung ein fundamentaler Bestandteil des Designprozesses für eine Programmiersprache ist.
Über den Lauf der Jahre wurden unterschiedliche Ansätze für verschiedene Sprachen und deren jeweiligen Anforderungen entwickelt. Das Verständis für die
internen Konzepte ist wichtig zur Bewertung von Performance und Tauglichkeit der Sprache bezüglich der jeweiligen Problemstellung. Dadurch zeigt sich,
dass die Betrachtung dieses Themas hohe Relevanz für Programmierer als auch
Entwickler und Designer neuer Sprachen aufweist.
Betrachten wir dazu als Motivation den Aufruf einer Funktion, welche uns den
Sinus eines Wertes berechnet. Dazu vergleichen wir eine exemplarischen Aufruf
in C++
std::sin(4.0);
mit einem äquivalenten Funktionsaufruf in PL/1
SIN(4);
Lässt man die geringfügigen syntaktischen Unterschiede der beiden Sprachen außer Acht, kann man aus Seiten des Programmiereres keinen klaren Unterschied
zwischen den beiden Aufrufen erkennen. Jener wird erst beim Übersetzen des
1
Programms sichtbar, wobei es sich im Falle von C++ um eine Bibliotheksfunktion handelt und bei PL/1 um einen built-in Funktion, welche sich in der Art
der Übersetzung stark unterscheiden.
Der Rest der Arbeit widmet sich dem Vergleich dieser unterschiedlicher Designansätze für APIs gegliedert in die folgenden Abschnitte:
• In Kapitel 2.1 wird genauer auf die Definition von APIs eingegangen insbesondere unter Berücksichtigung der Themenstellung.
• Eine Einführung in die beiden Designkonzepte der Implementierung der
API als Teil der Sprachsyntax (Kapitel 2.2) und als seperate Programmbibliothek (Kapitel 2.3).
• Vergleich dieser hinsichtlich mehreren Bewertungsfaktoren für Programmierer als auch unter dem Aspekt des Designs von Programmiersprachen
(Kapitel 2.4).
• Eine abschließende Bewertung der Ansätze im Bezug auf die vorrangegangenen Vergleichsanalysen erfolgt in Kapitel 3.
2
Sprachkonstrukte vs Bibliothek
In diesem Kapitel werden die beiden Designkonzepte für APIs vorgestellt, sowie
deren Vorkommen in den jeweiligen Programmiersprachen beschrieben, gefolgt
von einem Vergleich dieser bezüglich Performanz, Usability und Komplexität
der Implementierung.
2.1
Application Programming Interface
Eine API (Abkürzung für Application Programming Interface) beschreibt
grundlegend eine Schnittstelle zur Kommunikation mit anderen Systemen und
Programmen.
Sie stellt die jeweiligen Funktionsdeklerationen bereit, ist jedoch unabhängig
von der Implementierung der Funktionen.
Betrachetet man Scala standard library, welche auch die artihmentischen Operationen Addition, Multiplikation, etc. explizit implementiert, so kann man auch
die Operatoren +,-,/,.. als Teil der Sprach-API betrachen, welche nur ’syntactic sugar’ für die jeweiligen Funktionen darstellen[8].
2.2
Builtin Funktionen
Eine Möglichkeit der Implementierung einer API stellt die Bereitstellung von
builtin-functions dar. Diese bezeichnen von der Programmiersprache bereits vordeklarierte Funktionen, welche als keywords dem Programmierer zur Verfügung
gestellt werden und somit Teil der Syntax sind. Bekannte Programmiersprachen,
welche diese Form von Interface implementieren, sind unter anderem PL/1[6].
2
Dabei handelt es sich meist um grundlegenden Funktion wie artihmetische Operationen(sin, cos,..) und Zugriff auf Systemfunktionen(IO)[6]. Da die built-in
Funktionen Teil der Sprachsyntax sind, werden diese explizit vom Compiler
bzw. Interpreter behandelt. Wobei im Falle PL/1 die Funktionsaufrufe direkt in
architekturabhängige Maschinenbefehle übersetzt werden[4].
Dieses Konzept findet sich aber auch in neueren Programmiersprachen wie Go
wieder, welche häufig verwendete Operation wie map oder append als builtin
Funktion implementiert. Zudem haben die Entwickler, als weiteres Beispiel, ein
go-statement implementiert, welcher zur flexiblen und einfach Erzeugung von
asynchronen Routinen dient[2].
2.3
Programmbibliothek
Eine Programmbibliothek (engl. library) bezeichnet ein Modul, welches Funktionen, Teilprogramme und andere Konstrukte beinhaltet, welches wiederum von
anderen Programmen verwendet werden kann. Dieses Konzept ist weit verbreitet und findet sich in vielen Programmiersprachen wieder. Einige prominente
Vertreter sind unter anderem Haskell und C++.
Da die Funktionsdekleration oft unabhängig von der Definition ist, wird ein
zusätzlicher Schritt beim Compileprozess benötigt, um diese miteinandere zu
verbinden. Dieser Vorgang wird als Linking bezeichnet und kann auf unterschiedlichen Arten durchgeführt werden[9].
2.3.1
Static Linking
Bei statischen Bibliotheken handelt es sich um Bibliotheken welche beim Linking
in das Endprogramm statisch eingebunden werden. Dabei kopiert der Linker entweder die komplette Bibliothek oder auch nur benötigte Teilkomponenten ans
Ende des Programmcodes[10].
Dadurch steigt die Größe des kompilierten Programms um die Größe der Bibliothek. Durch das statische Einbinden ist das Programm jedoch nicht auf eine
systemeigene Bibliothek mehr angwiesen, da die jeweilige library schon mitgeliefert, was die Portabilität erhöht und unnötige Versionskonflikte vermeidet.
2.3.2
Dynamic Linking
Bei dynamischen Bibliotheken werden, im Gegensatz zu statischen, werden
die Modulkomponenten der jeweiligen Bibliothek erst zur Laufzeit des Programms in den Arbeitsspeicher geladen und stehen dann erst dem Programm
zur Verfügung[10]. Indem das Betriebssystem die Bibliothek in den virtuellen
Speicher lädt, unabhängig von dem jeweiligen Programm, können auch andere
Prozesse parallel auf dieselbe Bibliothek zugreifen, wodurch Systemressourcen
gespart werden können. Dies ist vorallem interressant für systemeigene Bibliotheken, da diese nur einmal geladen werden müssen und nicht die Größe des
Programms negativ beeinflussen[3]. Durch das dynamische Laden der Bibliothek ist lediglich eine Kompitibiltät der API für das Programm notwendig,
3
sodass die Bibliothek unabhängig davon verändert werden kann. In der Praxis
kann dies jedoch zu Konflikten kommen durch Abhängigkeit an unterschiedlichen Versionen. Gleichzeitig bietet dies aber auch eine einfache Möglichkeit des
Programmupdates im Gegensatz zum statischen Linking.
2.4
2.4.1
Vergleich der beiden Ansätze
Peformance
In diesem Kapitel wird genauer die Performanz der beiden Konzepte analysiert,
was vor allem für Softwareentwickler interessant ist.
Wenn eine Programmiersprache die jeweilige Funktion als Teil der Syntax implementiert ergeben sich mehrere Optimierungsmöglichkeiten für den Compiler.
Sie erlaubt eine optimierte Übersetzung in Maschinensprache, welches vor allem
in instruction count abhängigen Arbeitsfeldern, z.B. GPU-Shadern, von Vorteil
ist. So bieten Shadersprachen wie HLSL eine Vielzahl von built-in Funktionen
an zur Performanzverbesserung[5].
Dahingegen benötigt man bei Bibliotheken einen zusätlichen Funktionsaufruf,
welcher abhängig von der Art des Linking mit zusätzlichen Performanzkosten
verbunden sein kann (dynamic linking). Bei einige Sprachen wie C++ kann dies
vom Compiler durch Inlining der Funktion kompensiert werden. Zudem wurden im Laufe der Zeit weitere Möglichkeiten zur Optimierung der Perfomanz
entwickelt, unter anderem Link-Time-Optimization des GNU C Compiler. Dabei wird das Programm erst beim Linken im Gegensatz zur klassischen Method
beim kompilieren der .obj-Datei, was dem Compiler mehr Optionen lässt[7].
Daraus zeigt sich, dass Bibliotheksfunktionen im den meisten Fällen eine ähnliche Performanz bieten wie built-in Funktionen, da außerdem die Geschwindigkeit sehr stark von der jeweiligen Funktionsimplementierung abhängt.
2.4.2
Usability
Beim Arbeiten mit einer Programmiersprache ist es wichtig, dass sie intuitiv
und leicht benutzbar ist, um einen optimalen Workflow zu erreichen. Die Variante über die Syntax bietet durch die Unterstützung seitens des Compilers eine
Vorteil im Gegensatz zu Bibliotheksfunktionen. Da dem Compiler die Funktion
und der Implementierung bekannt ist, können zusätzliche Überprüfungen zur
Compiletime gemacht werden. Dies dient zur Prävention von Bugs und erleichtert die Wartung für den Programmierer. Hierbei illustriert Eberhard Sturm
in seinem Artikel ’Power vs. Adventure - PL/I and C’ ein gutes Beispiel für
Type Checking des Compilers in PL/1[11]: Er vergleicht die Verarbeitung der
Formatierungshinweise der C-Funktion printf mit dem Äquivalent put edit
aus PL/1. Hierbei sieht man, dass C keine Möglichkeit der Validierung der Parameter zulässt, da diese erst zur Laufzeit ausgewertet werden, im Gegensatz
zu PL/1, wo deren Typ bereits zur Übersetzungszeit betrachtet wird.
Ein weiterer Vorteil stellt eine mögliche Verbesserung der Lesbarkeit des Codes
dar. Durch Einfügen in die Keywordliste besteht die Option viel genutzte Funktionalitäten durch eine kompakte Syntax darzustellen, wie es am Beispiel der
4
go-Routinen sichtbar wird. Ein weiteres Beispiel SQL, womit mittels einer problemorientierte Syntax ohne zustäzlichem Einbinden von Bibliotheken effizient
Datenbankanfragen generiert werden können.
Bibliotheken hingegen glänzen durch Flexibilität und Austauschbarkeit, vor allem wenn dynamisch gebunden. Unterschiedliche Implementierungen können
einfach gewechselt werden durch linken einer anderen API-konformeen Bibliothek bei statischen Bibliotheken oder nur durch Laden einer neuen dynamischen
Bibliothek. Dies ermöglich z.B. das einfache Logging von Funktionsaufrufen,
welches beim Debuggen von OpenGL-Calls verbreitet ist[12].
Hierbei kann es mit Bibliotheken zu der bereits erwähnten Problematik des Versionings kommen, wo es zu Konflikten durch unterschiedliche Versionsnummern
kommen kann.
2.4.3
Implementierung
Wenn die API als Teil der Syntax implementiert ist, gibt es folglich eine hohe
Verflechtung zwischen Compiler und API, da die Funktionen der API keywords
sind, welche vom Compiler unterstützt werden müssen. Dies führt zu einer vergrößerten Komplexität hinsichtlich Wartung und Implementierung beim Compilerbau für diese Sprache, da zusätzlicher Aufwand im Parsing und Übersetzen
geleistet werden muss, was sich negativ auf die Größe des Compilers auswirkt.
Für den Programmierer bedeutet dies, dass bei Änderungen der API auch ein
Update des Compilers anfallen können.
Bei Bibliotheken wird, wie oben angesprochen, ein zusätzlicher Prozess beim
Kompilieren benötigt. Implementierung des Linkers und Loaders bedeutet
zusätzliche Mehrkosten, welche jedoch unabhängig von der jeweiligen API sind.
Dies führt zu einer besseren Entkopplung der Komponenten im Vergleich zu
dem API-Support mittels Keywords.
3
Bewertung
Vergleicht man die beiden Konzepte anhand der vorrangegangenen Analyse sieht
man, dass jedes für sich Vor- und Nachteile in den einzelnen Bereichen vorweisen kann. Deshalb ist es wichtig bei der Wahl des jeweiligen Designs genau die
Problemkategorie, welche durch die Programmiersprache gelöst werden soll, zu
betrachten.
Untersucht man die derzeitigen Trends und Entwicklung von Programmiersprachen so wird die API überwiegend durch Bibliotheksfunktionen in den ’moderneren’ Sprachen wie Scala, Rust, etc., bzw. eine Mischung aus beiden Konzepten
(siehe Go). Dies lässt sich auf ein modulares Design der Sprachen zurückführen,
welche eine relativ große Standard-API besitzen und in einzelne Komponenten
aufgeteilt wird[1].
Das Argument einer möglichen höheren Performance durch Syntax-Support
scheint nur in bestimmten Problembereichen wie zum Beispiel der angesprochenen Shaderprogrammierung zum Tragen zu kommen.
5
Literatur
[1] http://doc.rust-lang.org/0.12.0/std/.
[2] The Go Programming Language Specification. https://golang.org/ref/spec.
[3] Vorteile der Verwendung von DLLs.
de/library/dtba4t8b.aspx.
http://msdn.microsoft.com/de-
[4] Enterprise PL/I for z/OS: Enterprise PL/I Language Reference, 2003. Fifth
Edition.
[5] Reference
for
HLSL:
Intrinsic
Functions,
2014.
http://msdn.microsoft.com/en-us/library/windows/desktop/ff471376
[6] Paul Abrahams. The PL/I Programming Language. 3 1978.
[7] T. Glek and J. Hubicka. Optimizing real world applications with GCC link
time optimization. CoRR, abs/1010.2196, 2010.
[8] M. Odersky, S. Micheloud, N. Mihaylov, M. Schinz, E. Stenman, M. Zenger,
and et al. An overview of the scala programming language. Technical
report, 2004.
[9] L. Presser and J.R. White. Linkers and loaders. In ACM Computing
Surveys, volume 4, Nr. 3, pages 149–167. 9 1972.
[10] Michael L. Scott. Programming Language Pragmatics. Morgan Kaufmann
Pusblishers, Inc., 2000.
[11] Eberhard Sturm. Power vs. Adventure - PL/I and C. 10 1994.
[12] Damian Trebilco. https://code.google.com/p/glintercept/.
6
Herunterladen