nam: Objektnotation: this oder prototype?

Hi!

Wenn ich in Javascript ein Objekt definiere kann ich die Objektmethoden auf zwei Arten angeben:

1. (vereinfacht):

  
function Tree() {  
  this.branches={};  
  
  this.insert=function(k, v) {  
    //übersprungen  
  };  
  this.lookfor=function(k) {  
    //übersprungen  
  };  
}  

oder 2. (vereinfacht):

  
function Tree() {  
  this.branches={};  
}  
Tree.prototype.insert=function(k, v) {  
  //übersprungen  
};  
Tree.prototype.lookfor=function(k) {  
  //übersprungen  
};  

Unterscheiden sich die beiden Varianten irgendwie bezüglich Laufzeitverhalten, Speicherbedarf o.ä.?
Welche Schreibweise ist zu bevorzugen?

Danke und Gruss,
Mathias

  1. Moin.

    In Fall 1 weist du dem Objekt selbst im Konstruktor Eigenschaften zu - d.h., jedes Mal, wenn du per new ein neues Objekt erzeugst, muss der entsprechende Code ausgeführt werden.

    Im zweiten Fall weist du die Eigenschaften einmalig dem Prototyp zu. Dafür wird jedoch jeder Aufruf der Eigenschaft verzögert: Erst, nachdem die Eigenschaft im Objekt nicht gefunden wurde, wird der Prototyp zur Namensauflösung bemüht.

    » Welche Schreibweise ist zu bevorzugen?

    Kommt drauf an ;). Du kannst ja mal selbts tätig werden und überprüfen, wie oft du auf eine Eigenschaften zugreifen musst, damit der Unterschied in der Namensauflösung mesbar wird (vermutlich SEEEHR oft ;)).

    Meine persönliche Empfehlung wäre, alle Eigenschaften, die sich in den einzelnen Instanzen nicht voneinander unterscheiden (also insbesondere Konstanten und Methoden) dem Prototyp zuzuweisen und im Konstruktor tatsächlich nur Instanz-spezifische Berechnungen und Zuweisungen durchzuführen. Methodenzuweisungen würden sich also nur dann im Konstruktor finden, wenn du z.B. Zugriff auf per Closure bereitgestellte 'private' Eigenschaften benötigst.

    Christoph

    1. Nochmal ich.

      Das ist vielleicht nicht ganz klar geworden, daher hier nochmal explizit:

      Variante 1 erzeugt bei jeder Instanzierung von Tree neue Funktionsobjekte für die Methoden, was sich bei einer hinreichend großen Zahl von Instanzen natürlich im Speicherverbrauch bemerkbar macht, während bei Variante 2 alle Instanzen auf dasselbe FUnktionobjekt zugreifen!

      Christoph

    2. Hi

      Danke für deine Antwort. Habe das soweit verstanden.

      Du kannst ja mal selbts tätig werden und überprüfen, wie oft du auf eine Eigenschaften zugreifen musst, damit der Unterschied in der Namensauflösung mesbar wird (vermutlich SEEEHR oft ;)).

      Bei meinen ca 2000 Nodes kann ich keinen Unterschied messen.

      Meine persönliche Empfehlung wäre, alle Eigenschaften, die sich in den einzelnen Instanzen nicht voneinander unterscheiden (also insbesondere Konstanten und Methoden) dem Prototyp zuzuweisen[…]

      Das klingt einleuchtend. Nun kommt die Folgefrage:

      Gibt es ein Tool, das misst, wie viel Speicher ein JavaScript-Objekt aktuell belegt?

      Danke und Gruss,
      Mathias

      1. Kurtz gegrüßt

        Bei meinen ca 2000 Nodes kann ich keinen Unterschied messen.

        trotzdem würde ich die Methoden ins prototype legen, sicher ist sicher. Auf meiner alten Maschine hat exessives JS diverser Flugbörsen letztens schon swapping-Orgien mit Abstürzen verursacht. Auch die Prototype-Methoden kannst du bei Bedarf in ein Closure packen. Und wer weiß wieviele Nodes es einmal zukünftig sind.

        Beim Zugriff über Prototype ersparst du dir Speicheraufwand für minimal mehr Zeitaufwand.

        Gibt es ein Tool, das misst, wie viel Speicher ein JavaScript-Objekt aktuell belegt?

        Mir nicht bekannt, aber du kannst den Speicherverbrauch deines Browsers vorher/nachher messen. Aber sowas müsstest du konsequenterweise für alle Browser durchproben.

        Was du auf jeden Fall klären kannst ist, ob Functions die im Konstructor zugewiesen werden jedesmal neu *angelegt* werden. Prüf doch einfach ob

        obj1.methode1===obj2.methode1

        a) Falls true, dann haben deine 2000 Nodes nur jeweils 2 zusätzliche Referenzen auf die gleichen 2 Methoden. Also  ca zwischen 16-256 KB overhead für Namensraumeintrag und Referenzen.

        b) Falls false, dann wäre der Aufwand nochmal deutlich höher, weil 2*2000 komplette identische Methoden abgelegt werden müssten. Dann könntest du aber IMHO durch Codeumstellung Fall b) herbeiführen. (ungetestet, zumindest in Perl gäbs den erwünschten Effekt)

          
        function constructor () {  
          
          function methode () {  
          /* code */  
          }  
          
          this.meth_name=methode;  
        }  
        
        

        Grüße
         Kurt

        1. Moin.

          Was du auf jeden Fall klären kannst ist, ob Functions die im Konstructor zugewiesen werden jedesmal neu *angelegt* werden.

          Siehe mein Nachtrag.

          Dann könntest du aber IMHO durch Codeumstellung Fall b) herbeiführen.

            
          
          > function constructor () {  
          >   
          >   function methode () {  
          >   /* code */  
          >   }  
          >   
          >   this.meth_name=methode;  
          > }  
          > 
          
          

          So nicht, sondern so:

            
          function methode () {  
          /* code */  
          }  
          function constructor () {  
            this.meth_name=methode;  
          }  
          
          

          Christoph

          1. Kurtz gegrüßt

            So nicht, sondern so:

            ja hab ich gerade auch eruiert ... : )

              
            function meth3 () {  
              alert("meth3")  
            }  
              
            function cnst () {  
              
              function meth1 () {  
                alert("meth1")  
              }  
              
              this.meth1=meth1;  
              this.meth2=function () {alert("meth2")};  
              this.meth3=meth3;  
            }  
              
            a=new cnst();  
            b=new cnst();  
              
            alert(a.meth1==b.meth1); // false  
            alert(a.meth2==b.meth2); // false  
            alert(a.meth3==b.meth3); // true  
              
            a.meth1();  
            b.meth1();  
            
            

            interessant in Perl werden geschachtelte Subs nur einmalig kompiliert ... hmm ...

            Grüße
             Kurt

            1. Hi

              Danke für eure Antworten.
              Sie decken sich mit den Aussagen eines anderen Dokuments, welches ich inzwischen gefunden habe.

              Ich konnte das auf allen getesteten Plattformen nachvollziehen (ich befürchtete das das nicht alle Browser so handhaben).

              Über den tatsächlichen Speicherbedarf und ob die Interpreter da eventuell etwas optimieren, habe ich noch keine endgültige Antwort gefunden.

              Gruss,
              Mathias

              1. So!

                Habe eben mit Instruments aufm Mac verglichen, wie viel Speicher verschiedene Browser allozieren:

                +-----------------+---------+-----------+
                |Browser          | this    | prototype |
                +-----------------+---------+-----------+
                |Opera 9.5b:      | 19.42MB |  12.8MB   |
                |Firefox 2.0.0.13:| 26.19MB |  21.8MB   |
                |Firefox 3b5:     | 15.55MB |  15.0MB   |
                |Safari 3.1.1:    |  5.97MB |   5.8MB   |
                +-----------------+---------+-----------+

                Den IE kann ich leider nicht ausmessen.
                Die gemessenen Unterschiede bei Opera und Firefox 2 (4-6MB) stimmen in etwa mit der erwarteten Menge überein.

                Interpretation:
                Safari und Firefox 3 optimieren da offensichtlich etwas. Trotzdem lohnt es sich tatsächlich Methoden über den Prototypen zu definieren (falls möglich).

                Gruss,
                nam

                1. Kurtz gegrüßt

                  +-----------------+---------+-----------+
                  |Browser          | this    | prototype |
                  +-----------------+---------+-----------+
                  |Opera 9.5b:      | 19.42MB |  12.8MB   |
                  |Firefox 2.0.0.13:| 26.19MB |  21.8MB   |
                  |Firefox 3b5:     | 15.55MB |  15.0MB   |
                  |Safari 3.1.1:    |  5.97MB |   5.8MB   |
                  +-----------------+---------+-----------+

                  7MB bei 2000 Nodes ist schon der Hammer. Aber wie hast du die Methoden für this zugewiesen? Jedesmal  neue Methoden angelegt (meth1) oder nur wie von uns vorgeschlagen die fixe Referenz zu  statischen Methoden kopiert (meth3)?

                  Ganz kann ich dein Ergebnis nicht allerdings nicht nachvollziehen, weil mit meth3 ist der Speicheroverhead zwar größer, im Objekthash jedes Nodes kommen 2 zusätzliche Einträge, aber nicht dramatisch. (angenommen ein Eintrag kostet 50 Bytes dann sind dass 200 KB)

                  FRAGEN:
                  1. Hast du mehr als 2000 Nodes ausprobiert?
                  2. meth1 oder meth3?
                  3. Handelt es sich bei deinen "Nodes" um DOM-Objekte oder simple selbstdefinierte JS-Objekte?

                  Grüße
                   Kurt

                  1. Hi

                    Die Zahlen in der 'this'-Spalte beziehen sich auf die 1. Variante im Eingangsposting (deine meth2).

                    Die 'prototype'-Spalte entsprechend der 2. Variante des Eingangspostings: die Methoden werden nicht im Tree-Objekt sondern im Tree.prototype-Objekt abgelegt. Damit dauert die Identifier Resolution einen Tick länger, das Tree-Objekt (das ja ca 2000fach und verschachtelt vorkommt) ist aber kleiner.

                    Deine meth3 habe ich nicht implementiert, da meine 2. Variante (Tree.prototype) vermutlich noch besser ist.

                    Ganz kann ich dein Ergebnis nicht allerdings nicht nachvollziehen, weil mit meth3 ist der Speicheroverhead zwar größer, im Objekthash jedes Nodes kommen 2 zusätzliche Einträge, aber nicht dramatisch. (angenommen ein Eintrag kostet 50 Bytes dann sind dass 200 KB)

                    meth3 ist ja auch die sparsamste von deinen 3 Methoden.

                    1. Hast du mehr als 2000 Nodes ausprobiert?

                    Nein.

                    1. meth1 oder meth3?

                    s.o.

                    1. Handelt es sich bei deinen "Nodes" um DOM-Objekte oder simple selbstdefinierte JS-Objekte?

                    Letzteres.

                    Ich werd mal ne Test-Page zusammenstiefeln und den Link hierhinstellen.
                    Kann gut sein, dass ich was verbockt habe :-/

                    1. Kurtz gegrüßt

                      Ich werd mal ne Test-Page zusammenstiefeln und den Link hierhinstellen.
                      Kann gut sein, dass ich was verbockt habe :-/

                      Ach was ... und wenn mehrere Leute den gleichen nachprüfbaren Test auf Ihren Systemen ausführen können, wärs schon interessant.

                      Es kann übrigens gute Gründe geben in this Methoden anzulegen (z.B. closures mit Instanzvariablen, überladen der Methode,...), der von dir beschriebene Speicheroverhead bei einem relativ simplen Fall mahnt aber  zur Vorsicht.

                      Dauerswappende Webseiten sind keine Empfehlung...

                      Wenn nicht prototype würde ich deswegen meth3 bevorzugen.

                      Danke für die Info : )

                      Grüße
                       Kurt

                      1. Kurtz gegrüßt

                        Anmerkung, du könntest testen ob der Code in https://forum.selfhtml.org/?t=170303&m=1113387 in FF3 die gleichen Ergebnisse liefert... hab leider keinen FF3 da.

                        Grüße
                         Kurt

                      2. Es kann übrigens gute Gründe geben in this Methoden anzulegen (z.B. closures mit Instanzvariablen, überladen der Methode,...), der von dir beschriebene Speicheroverhead bei einem relativ simplen Fall mahnt aber  zur Vorsicht.

                        Oder um Variabeln zu kapseln.

                        Dauerswappende Webseiten sind keine Empfehlung...

                        Ich halte den Speicherverbrauch für zu hoch, ich komm hier auf einem WinXP System mit 90.000 Objekten auf ca. 10 MB(FF) und 100MB (IE 7), allerdigns nur grob geschätzt mit dem Taskmanager.

                        Struppi.

                      3. Hi

                        wenn mehrere Leute den gleichen nachprüfbaren Test auf Ihren Systemen ausführen können, wärs schon interessant.

                        Quick and dirty: http://www.mnn.ch/diversa/Trie/Trie.html

                        Es kann übrigens gute Gründe geben in this Methoden anzulegen (z.B. closures mit Instanzvariablen, überladen der Methode,...)

                        Deswegen hatte ich das ja anfangs so geschrieben; ist aber in diesem Fall nicht nötig.

                        Würde mich freuen, wenn du mal meine Messdaten bestätigen/widerlegen, jedefalls überprüfen könntest.
                        Eine Datei Trie3.js nach deiner methode3 folgt.

                        Gruss,
                        Mathias

      2. Das klingt einleuchtend. Nun kommt die Folgefrage:

        Gibt es ein Tool, das misst, wie viel Speicher ein JavaScript-Objekt aktuell belegt?

        Wozu?

        JS ist ja kein Sprache mit der du Speicherverwaltung machen müßtest, auch ist sie für große Anwendungen nur bedingt geeignet. Aber 2000 Objekte sollten erstmal kein Problem sein. Für die Geschwindigkeit dürfte prototype von Vorteil sein, aber der Flaschenhals sind immer DOM Zugriffe, bei denen der Browser auch gerne mal eine Pause einlegt.

        Struppi.

        1. Hi Struppi

          Wozu?

          Aus Interesse und Hang zum Perfektionismus.

          JS ist ja kein Sprache mit der du Speicherverwaltung machen müßtest,[…]

          Trotzdem kann man offenbar (wenn ich alles richtig verstanden habe) Code schreiben, der mehr oder weniger viel Speicher belegt.

          1. Wozu?
            Aus Interesse und Hang zum Perfektionismus.

            Ich könnte das nachvollziehen in einer Programmiersprache wo du auch tasächlich Einfluss auf solche Dinge hast, aber in JS ist die Speicherverwaltung Aufgabe des Browsers insofern in keinsterweise unter deiner Kontrolle.

            Aber es ist klar, dass je mehr Objekte du erzeugst, mehr Speicher verbraucht wird, bisher war das ja deine Fragestellung.

            Struppi.

            1. aber in JS ist die Speicherverwaltung Aufgabe des Browsers insofern in keinsterweise unter deiner Kontrolle.

              Das ist mir klar. Aber es ist interessant zu sehen, dass und wie die Browser sich unterschiedlich verhalten, was die Speicherverwaltung betrifft und auch wenn ich nicht beeinflussen kann, wie der Speicher verwaltet wird: ich kann sehr wohl beeinflussen wieviel Speicher meine Objekte belegen.

              Aber es ist klar, dass je mehr Objekte du erzeugst, mehr Speicher verbraucht wird, bisher war das ja deine Fragestellung.

              Nein, meine Fragestellung war, ob es einen Unterschied macht, ob ich die Methoden im Konstruktor des Objekts definiere (siehe OP, Variante 1) oder im Prototype des Objekts (Variante 2).
              Dass mehr Objekte auch mehr Speicher brauchen ist mir klar.

              Gruss,
              Mathias

              1. Aber es ist klar, dass je mehr Objekte du erzeugst, mehr Speicher verbraucht wird, bisher war das ja deine Fragestellung.

                Nein, meine Fragestellung war, ob es einen Unterschied macht, ob ich die Methoden im Konstruktor des Objekts definiere (siehe OP, Variante 1) oder im Prototype des Objekts (Variante 2).
                Dass mehr Objekte auch mehr Speicher brauchen ist mir klar.

                Und die Variante 1 erzeugt wesentlich mehr Funktionsobjekte, wie ja Kurt bereits aufgezeigt hat.

                Struppi.

  2. Im Folgenden bezeichne n die Zahl der Instanzen von Klasse.

    --- Variante 1 ---

      
    function Klasse() {  
     this.methode = function() {};  
    }  
    
    

    Laufzeitverhalten =
      n * (Erzeugung Funktionsobjekt + Zuweisung an Instanz-Eigenschaft)

    Speicherverbrauch =
      n * (Funktionsobjekt + Referenz in Instanz + Closure)

    Variante 1 erlaubt den Zugriff auf in Klasse lokale Variablen. Dazu muss eine Closure erzeugt und im Speicher gehalten werden. Erfolgt kein Zugriff auf lokale Variablen, sollte diese vom Garbage Collector entsorgt werden.

    --- Variante 2 ---

      
    function methode() {}  
    function Klasse() {  
     this.methode = methode;  
    }  
    
    

    Laufzeit:
      (Erzeugung Funktionsobjekt + Zuweisung an globale Eigenschaft)

    • n * (Zuweisung an Instanz-Eigenschaft)

    Speicherverbrauch:
      (Funktionsobjekt + Referenz in window)

    • n * (Referenz in Instanz)

    --- Variante 3 ---

      
    function Klasse() {}  
    Klasse.prototype.methode = function() {};  
    
    

    Laufzeit:
      (Erzeugung von Funktionsobjekt + Zuweisung an Prototyp-Eigenschaft)

    • (Zahl der Methodenaufrufe) * (Namensauflösung im Prototyp)

    Speicherverbrauch:
      (Funktionsobjekt + Referenz im Prototyp)

    Es wäre vielleicht wirklich interessant, sich in den Quelltext von SpiderMonkey einzulesen, um einschätzen zu  können, was (zumindest im Firefox) 'unter der Haube' abläuft...

    Christoph

    1. Und hier die schamlos von nam kopierten Messergebnisse:

      +------------------+--------------------+--------------------+--------------------+
      |Browser           |Variante 1: Trie.js |Variante 2: Trie3.js|Variante 3: Trie2.js|
      +------------------+---------+----------+---------+----------+---------+----------+
      |                  |  t(ms)  |  V(MB)   |  t(ms)  |  V(MB)   |  t(ms)  |  V(MB)   |
      +------------------+---------+----------+---------+----------+---------+----------+
      |Firefox 2.0.0.13: |   360   | 23.6     |  177    |  20.40   |   187   |  19.61   |
      +------------------+---------+----------+---------+----------+---------+----------+
      |Firefox 3b5:      |   237   | 25.16    |  178    |  22.74   |   129   |  21.61   |
      +------------------+---------+----------+---------+----------+---------+----------+
      |Opera 9.26:       |  1211   | 23.07    |  990    |  18.61   |  1173   |  16.52   |
      +------------------+---------+----------+---------+----------+---------+----------+
      |Safari 3.1.1:     |    91   |  6.80    |   62    |   6.22   |    60   |   6.40   |
      +------------------+---------+----------+---------+----------+---------+----------+

      Christoph

      1. Hi Christoph

        Danke für die Zusammenfassung. Da seh ich jetzt auch klarer!
        Vor allem die theoretischen Berechnungen zu Laufzeit und Speicherbedarf machen den internen Ablauf nochmals deutlich.

        Die von mir gemessenen Daten bedürften einer Überprüfung. Wäre spannend zu sehen, ob sie sich bestärken lassen.

        Interessant wäre eine Messreihe unter Windows, wo auch der IE vorhanden ist und man somit alle Browser miteinander vergleichen könnte.

        Wie misst man den Speicherbedarf einer Applikation unter Windows etwas genauer, als mit dem Taskmanager?

        Gruss,
        nam

        1. Moin.

          Danke für die Zusammenfassung. Da seh ich jetzt auch klarer!
          Vor allem die theoretischen Berechnungen zu Laufzeit und Speicherbedarf machen den internen Ablauf nochmals deutlich.

          Wobei es sich bei den theoretischen Überlegungen - z.B. was den Speicherverbrauch für Referenzen betrifft - um Milchmädchenrechnungen handelt: Aller Wahrscheinlichkeit nach werden die Objekteigenschaften in Hashtables abgelegt, d.h., unabhängig davon, ab die Referenz exisitiert oder nicht, muss Speicherplatz für die im Zweifelsfall leeren Buckets alloziert werden.

          Ähnlich verhält es sich mit den Überlegungen zu der Closure in Variante 1: es kann sein, dass hier ein neues Objekt erstellt wird, ebenso ist es aber auch möglich, dass der sowieso vorhandene Kontext weitervewendet und nur einem Pointer zugewiesen werden muss. Mal ganz abgesehen davon, dass der aktuelle Speicherverbrauch natürlich stark vom verwendeten Garbage-Collection-Mechanismus abhängt...

          Um hier mit Gewissheit argumentieren zu können, müsste man tatsächlich mal (ECMA-)Spezifikationen und Quelltexte wälzen. Interessieren würde mich das schon, da ich mich selbst schon mal an einem Interpreter für eine ähnlich dynamische Sprache versucht habe, aber momentan habe ich nicht die Zeit, mich im Detail damit auseinanderzusetzen...

          Christoph