Henry: Funtion in Funktion Unterschiede Closures

problematische Seite

Hallo,

ich fand diesen Artikel, aber die Beispiele leuchten mir nicht ein.

function initAlert () {
   let msg = "Was noch zu sagen wäre";
   window.setTimeout (function () {alert (msg); }, 100);
}

ist langsamer als


function initAlert () {
    window.setTimeout ( function () {
        let msg = "Was noch zu sagen wäre";
        alert (msg);
    }, 100);
}

Aus welchem Grund soll die erste Version erheblich langsamer sein? Und ist das wirklich so?

Gruss
Henry

--
Meine Meinung zu DSGVO & Co:
„Principiis obsta. Sero medicina parata, cum mala per longas convaluere moras.“

akzeptierte Antworten

  1. problematische Seite

    Ich brauche Closures nicht, aber sie interessieren mich trotzdem. Meine Antwort könnte also auch daneben liegen, dann bitte ich um Korrektur.

    Die (namenlose) Funktion die alert aufruft, läuft unabhängig von initAlert. Wenn die Message ausgegeben wird, ist initAlert längst beendet und die Variable msg ist damit auch wieder weg. Diese Variable muss also für die spätere Ausführung der namenlosen Funktion konserviert werden.

    Im zweiten Beispiel ist diese Variable Teil der namenlosen Funktion. Zur Ausführung der namenlosen Funktion muss die Variable also nicht erst aus dem Geltungsbereich der äußeren Funktion "umgelagert" werden.
    Ich vermute nun, die Konservierung dieser Variable für den späteren Zeitpunkt kostet den erwähnten Mehraufwand.

    Ob das allerdings wirklich so erheblich ist und in der Praxis tatsächlich ein Problem wird?

  2. problematische Seite

    Tach!

    ich fand diesen Artikel, aber die Beispiele leuchten mir nicht ein.

    function initAlert () {
       let msg = "Was noch zu sagen wäre";
       window.setTimeout (function () {alert (msg); }, 100);
    }
    

    ist langsamer als

    function initAlert () {
        window.setTimeout ( function () {
            let msg = "Was noch zu sagen wäre";
            alert (msg);
        }, 100);
    }
    

    Aus welchem Grund soll die erste Version erheblich langsamer sein?

    Die Frage zu erörtern ist müßig. Entweder braucht man die Funktionalität der Variable im äußeren Scope, dann muss man mit eventuell vorhandenen Einschränkungen leben. Oder man braucht sie nicht und kann sie lokal lassen.

    Das zeitliche Verhalten braucht man meist nur in zeitkritischen Situationen zu beachten. Ansonsten verliert sich der Unterschied üblicherweise im Grundrauschen.

    Ob wirklich ein Unterschied besteht, kann ich nicht sagen. Im ersten Fall wird die Variable im äußeren Scope nur einmal angelegt, aber bei jedem Callback wird sie im inneren und dann im äußeren Scope gesucht. Im zweiten Fall wird sie zwar beim Callback nur im inneren Scope gesucht, dafür aber immer wieder erneut angelegt. Das wird sehr abhängig von der jeweiligen Javascript-Engine sein, und wie Zugriffe auf die Scope Chain optimiert sind und wie Closures von der Engine im Allgemeinen gehandhabt werden.

    Es kann sogar ein Vorteil sein, die Variable außen anzulegen, auch wenn sie dort nicht benötigt wird und auch intern nicht verändert wird. Das wäre dann der Fall, wenn die Ermittlung des Wertes aufwendig ist, wie beispielsweise die Suche eines Elements im DOM.

    Und ist das wirklich so?

    Aufgrund des alert ist das nicht messbar. Wenn das durch etwas ohne Nutzerinteraktion ersetzt wird, und es faktisch einen Unterschied gibt, wird er praktisch nicht relevant sein.

    Außerdem scheint mir der Artikel trotz der Datierung auf 2018 schon etwas älter zu sein. Funktionen herumzureichen ist schon lange kein besonderes Merkmal von Javascript mehr. Und neben dem dort erwähnten Javascript-Interpreter gibt es schon länger Engines mit Kompilierfunktion, so dass man nicht mehr davon ausgehen kann, dass Javascript immer interpretiert wird.

    dedlfix.

    1. problematische Seite

      Hi,

      Funktionen herumzureichen ist schon lange kein besonderes Merkmal von Javascript mehr.

      war es das jemals? In C konnte man doch auch schon Functions als Parameter nutzen (ok, waren pointer auf die function), lange bevor überhaupt über die Erfindung von Javascript nachgedacht wurde …

      cu,
      Andreas a/k/a MudGuard

      1. problematische Seite

        Tach!

        Funktionen herumzureichen ist schon lange kein besonderes Merkmal von Javascript mehr.

        "Besonders", im Sinne von "kleine Gruppe", nicht "ausschließlich".

        Der eigentliche Satz in dem Artikel heißt: "In Javascript dürfen Funktionen in Funktionen erzeugt werden – ein Feature, dass es nur in wenigen Programmiersprachen gibt." Das bezog sich also nicht auf das herumreichen, sondern auf das schachteln. Das hatte ich mir nicht richtig gemerkt. Aber auch so ist diese Aussage zweifelhaft, oder ich kenne all diese "nur wenigen Programmiersprachen". Tatsächlich ist das ein doch recht verbreitetes Feature.

        war es das jemals? In C konnte man doch auch schon Functions als Parameter nutzen (ok, waren pointer auf die function), lange bevor überhaupt über die Erfindung von Javascript nachgedacht wurde …

        Aber nehmen wir mal an, die Aussage hätte sich auf das Herumreichen bezogen ... auch das ist in vielen Sprachen vorhanden/angekommen, weil das für funktionales Programmieren asynchrones Programmieren (Callbacks) benötigt wird.

        dedlfix.

      2. problematische Seite

        Hallo Andreas,

        wenn Du es so siehst, gibt es Funktionen als Parameter schon, seit es die von Neumann Architektur gibt. Eine Einsprungadresse zu bestimmen und herumzureichen ging im Assembler schon immer und Dennis Ritchie hat das in diesen monströsen Makroassembler namens C vererbt.

        Sehr viele Programmiersprachen stellen die Adresse eines Codestücks jedenfalls nicht als Wert zur Verfügung und erlauben auch keine Call-Operation auf einen Wert. Aus der wohlbegründeten Paranoia heraus, dass dieser Wert eine ungültige Adresse darstellen könnte.

        Ideen, wie man sowas auf halbwegs sichere Weise tun kann, kamen dann erst später. Die historische Entwicklung beginnt laut en.wikipedia 1970 mit der Programmiersprache PAL und setzt sich in Scheme fort. Auch in Common Lisp und Smalltalk findet man Code als Objekt erster Klasse schon früh.

        Dass eine Funktion auch ihren Kontext mitnimmt, also eine Closure bildet, ist ein Schritt mehr. Viele Sprachen bieten Funktionszeiger oder Methodenzeiger an, sie unterstützen auch Callbacks, das Ankleben eines Kontextes ist aber nicht so verbreitet.

        Rolf

        --
        sumpsi - posui - obstruxi
  3. problematische Seite

    Hallo Henry,

    also grundsätzlich hat die Ulrike aus Moers schon recht. Closures sind Mehraufwand. Aber schauen wir uns das mal näher an.

    Im folgenden Beispiel gibt es drei Scopes:

    • den globalen Scope
    • den Scope von createAdder
    • den Scope von doAdd
    function createAdder(x) {
       let garbage = new Array(1000);
       garbage.fill(4711);
    
       function doAdd(y) {
          return x+y;
       }
       return doAdd;
    }
    
    let add7 = createAdder(7);
    let add11 = createAdder(11);
    let add99 = createAdder(99);
    
    console.log("4+7 = " + add7(4));
    

    Jeder dieser Scopes wird beim Aufruf seines Scope-Trägers erzeugt und lebt so lange, wie eine lebendige Referenz auf ihn existiert. Bei globalen Scope ist der Träger die JS-Engine selbst. Der Scope von createAdder wird beim Aufruf der Funktion erzeugt. Er enthält vier Einträge:

    • eine Referenz auf seinen Besitzer, das Funktionsobject createAdder
    • den Parameter x
    • das Array garbage
    • ein Funktionsobjekt mit dem Namen doAdd.

    Zu beachten ist hier, dass bei jedem Aufruf von createAdder das doAdd Funktionsobjekt neu entsteht. Die JS-Engine mag es fertigbringen, den ausführbaren Code für doAdd herauszufaktorieren und nur einmal zu speichern, aber das Funktionsobjekt an sich entsteht jedes Mal neu. Deswegen kann man createAdder mehrfach aufrufen und drei Funktionen bekommen, die 7, 11 und 99 addieren.

    Und deswegen hat Ulrike recht, wenn sie sagt, dass das langsamer ist als eine statische Funktion.

    Wenn das Funktionsobjekt für doAdd angelegt wird, speichert es sich eine Referenz auf den Scope, in dem es erzeugt wurde. Und wenn createAdder das doAdd Objekt zurückgibt, dann nimmt es diese Scope-Referenz mit. Eine Referenz auf den ganzen Scope, nicht nur auf den Parameter x!

    Die Rückgabe von doAdd aus createAdder bewirkt, dass der Scope des createAdder Aufrufs nach Ende des Funktionsaufrufs noch eine lebendige Referenz hat. Deshalb bleibt er erhalten und wird nicht vom Garbage Collector entsorgt. Rein formal bleibt deshalb auch das garbage-Array erhalten, und belegt Speicher. Eine gute JS-Engine könnte feststellen, dass nur x benötigt wird, und den Scope teilen. Ich weiß aber nicht, ob Spidermonkey oder V8 dazu im Stande sind.

    In PHP ist das übrigens anders gelöst. Wenn ich da eine Closure bilden will, muss ich das explizit mit der use-Klausel tun. Auf diese Weise werden nur die Daten in der Closure gebunden, die unbedingt gebraucht werden.

    Wird nun beispielsweise add7 aufgerufen, so wird wieder ein neuer Scope erzeugt. Er enthält:

    • eine Referenz auf seinen Besitzer, dass Funktionsobjekt in add7
    • den Parameter y

    Beim Ausführen der Funktion wird die Variable x benötigt. Und nun wird es spannend, denn hier besteht viel Optimierungspotenzial.

    Ein dummer Interpreter würde nun die Scope-Chain nach einer Variablen mit dem Namen "x" absuchen. Er beginnt im aktuellen Scope, nicht gefunden. Er geht zum Parent-Scope, was zwei Schritte braucht: (1) hol Dir Deinen Besitzer und (2) frag deinen Besitzer nach dem Scope, in dem er liegt. Im Parent-Scope wird nun wieder eine Variable mit dem Namen "x" gesucht, gefunden.

    Und deswegen hat Ulrike recht, wenn sie sagt, dass der Zugriff auf eine Variable im Elternscope langsamer ist als auf eine Variable im eigenen Scope. Sie hat auch recht mit dem Memory Leaks. Ein Scope, der nicht entsorgt wird, belegt Speicher. Eine Eventhandler-Funktion, an der eine fette Scope-Kette hängt, verhindert ggf. das Abräumen einer Menge von nicht mehr benutzten Daten. Man muss da schon aufpassen.

    Aber um wieviel langsamer ist das?

    Ein klügerer Interpreter compiliert seinen Code zunächst. Und sei es auch nur in einen Bytecode, wie PHP. V8 dagegen enthält einen JIT-Compiler, der echten Maschinencode erzeugt. Ein klügerer Interpreter stellt da schon fest, dass x nicht im eigenen Scope liegt. Da die Schachtelung der Scopes rein lexikalisch erfolgt, kann er beim Compileren bereits feststellen, dass y im aktuellen Scope (Scope 0) und x im direkten Elternscope (Scope +1) liegen, und kann Bytecode generieren, der x direkt aus Scope +1 und y aus Scope 0 holt. Er kann bei längeren Scope-Ketten auch Code generieren, der die Referenzen auf diese Scopes zu Beginn der Funktionsausführung ermittelt und sie im Scope 0 ablegt (und er kann es lassen, wenn er feststellt, dass sich das nicht lohnt). Ein kluger Interpreter muss auch nicht namentlich nach Variablen suchen. Statt dessen weiß er, dass mit let, const und var ein bestimmter Satz von Variablen angelegt wurde. Er weiß, dass x im Scope +1 der zweite Eintrag ist und dass y im Scope 0 ebenfalls der zweite Eintrag ist. Er generiert also solchen Bytecode:

    • push (+1, 2)
    • push (0, 2)
    • add

    Heißt (diese Bytecodes arbeiten stackorientiert):

    • Schiebe die 2. Variable aus Scope +1 auf den Stack
    • Schiebe die 2. Variable aus Scope 0 auf den Stack
    • Addiere die beiden obersten Werte auf dem Stack

    Und das ist überhaupt nicht mehr langsam. Es ist langsamer, aber nur um ein paar Nanosekunden. Das fällt auf, wenn solcher Code in Schleifen mit hoher Ausführungszahl abläuft. In normalem eventverarbeitenden Code, was JavaScript im Browser ist, merkt das kein Mensch. Das sieht anders aus, wenn man mit JavaScript hochperformanten Code erzeugen muss, z.B. für einen stark belasteten node.js Server. In Code-Hotspots Closures einzusetzen kann da bedeuten, dass der Server nicht mehr 1000 Requests pro Sekunde schafft, sondern nur noch 900 oder noch weniger. Je nachdem, wie hot der spot ist und wieviel I/O der Code nebenbei noch macht.

    Mit dem Memory Leaks ist es auch im Browser kritisch. Mein garbage-Array wäre ein echtes Problem. Oder anders: Wenn ich 7 Funktionen ineinander schachtele und ganz tief drinnen fleißig Eventhandler registriere, baumeln da eine Menge Scopes dran.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. problematische Seite

      Hallo Rolf,

      schon fast ne kleine Dissertation. 😉 Danke.

      Gruss
      Henry

      --
      Meine Meinung zu DSGVO & Co:
      „Principiis obsta. Sero medicina parata, cum mala per longas convaluere moras.“