2013-11-27 12 views
5

Das Problem:Effiziente Anfügen zu einem variabler Länge Behälter von Strings (Golang)

ich mehrere reguläre Ausdrücke, die jeder Zeile einer großen Protokolldatei (wie mehrere GB lang) anwenden müssen, sammeln nicht leer Spiele und setze sie alle in ein Array (für die Serialisierung und das Senden über das Netzwerk).

Scheiben sind nicht viel helfen, wenn Antwort auf this question gilt:

Wenn die Scheibe nicht genügend Kapazität hat, hängen Sie müssen neue Speicher zuweisen und die alte über kopieren. Für Slices mit < 1024 Elementen wird die Kapazität verdoppelt, für Slices mit> 1024 Elementen wird sie um den Faktor 1,25 erhöht.

Da es buchstäblich Hunderttausende von Regex-Übereinstimmungen geben kann, kann ich die Länge/Kapazität einer Schicht nicht wirklich vorhersagen. Ich kann es nicht zu groß machen "nur für den Fall" bc das wird Speicher verschwenden (oder wird es? Wenn Speicherzuweiser ist schlau genug, nicht zu viel Speicher, der nicht in geschrieben wird, vielleicht könnte ich eine riesige Slice-Kapazität verwenden ohne viel Schaden?).

Ich denke über folgende alternative So:

  1. haben eine doppelt verknüpfte Liste der Spiele (http://golang.org/pkg/container/list/)
  2. ber seine Länge (? Wird len() Arbeit)
  3. preallocate ein Stück dieses Kapazität
  4. Kopie Zeichenfolge Zeiger auf diese Scheibe

gibt es eine weniger Arbeit den Weg, dieses Ziel in Go zu erreichen (anhängen mit ~ O (1) Komplexität anhängen)?

(golang Neuling hier natürlich)

Antwort

13

append() ‚s durchschnittlich (abgeschrieben) Kosten ist bereits O (1), weil es wächst das Array von einem jedes Mal Prozentsatz. Wenn das Array größer wird, wird es teurer, aber proportional seltener. Ein 10M-Stück Slice ist 10x teurer zu wachsen als ein 1M-Stück Slice, aber da die zusätzliche Kapazität, die wir zuweisen proportional zur Größe ist, wird es auch 10x so viele append(slice, item) Aufrufe bis zum nächsten Mal wächst . Die steigenden Kosten und die abnehmende Häufigkeit von Neuzuordnungen heben sich auf, wodurch die Durchschnittskosten konstant bleiben, d. H. O (1).

Die gleiche Idee gilt auch für dynamisch skalierte Arrays anderer Sprachen: Microsofts std::vector Implementierung vergrößert das Array zum Beispiel jedes Mal um 50%. Amortisiertes O (1) bedeutet nicht, dass Sie nichts für Allokationen zahlen, sondern nur, dass Sie weiterhin mit der gleichen durchschnittlichen Rate zahlen, wie Ihr Array größer wird.

Auf meinem Laptop konnte ich eine Million slice = append(slice, someStaticString) s in 77ms laufen. Ein Grund, warum es schnell ist, was Siritinga unten bemerkt, ist, dass das "Kopieren" der Zeichenfolge, um das Array zu vergrößern, nur den String-Header (ein Pointer/Längen-Paar) kopiert und den Inhalt nicht kopiert. 100.000 Zeichenfolgen-Header sind immer noch weniger als 2 MB zum Kopieren, keine große Sache im Vergleich zu den anderen Datenmengen, mit denen Sie arbeiten.

container/list stellte sich heraus ~ 3x langsamer für mich in einem microbenchmark; linked-list append ist natürlich auch konstante Zeit, aber ich stelle mir vor append hat eine niedrigere Konstante, weil es in der Regel nur ein paar Worte des Gedächtnisses schreiben und nicht ein Listenelement usw. zuweisen kann. Der Timing-Code wird nicht funktionieren in der Spielplatz, aber Sie können diese lokal kopieren und führen Sie es selbst sehen: http://play.golang.org/p/uYyMScmOjX


aber Sie fragen eine spezifischere Frage hier über eine grep -ähnlichen Anwendung (und vielen dank für eine detaillierte Frage mit Kontext zu fragen). Aus diesem Grund ist die beste Empfehlung, wenn Sie Gigs von Logs suchen, ist es wahrscheinlich am besten zu vermeiden, die gesamte Ausgabe im RAM zu puffern.

Sie könnten etwas schreiben, um die Ergebnisse als eine einzige Funktion zu streamen: logparser.Grep(in io.Reader, out io.Writer, patterns []regexp.Regexp); Sie könnten alternativ out eine chan []byte oder func(match []byte) (err error) machen, wenn Sie nicht möchten, dass der Code, der Ergebnisse sendet, zu eng mit dem Grep-Code verknüpft ist.

(On []byte vs. string: a []byte scheint die Arbeit hier zu tun und vermeidet []byte < =>string Conversions, wenn Sie mir/O, also würde ich, dass ich lieber nicht wissen, was alles, was Ihnen‘. re tun, aber, und wenn Sie string brauchen es ist in Ordnung.)

wenn Sie haltet die ganze Spiel Liste im RAM tun , beachten Sie, dass auf einen Teil einer großen String oder Byte-Scheibe um eine Referenz hält die ganze hält Source String/Slice von Garbage Collection gesammelt. Also, wenn Sie diese Route gehen, dann können Sie tatsächlich Counterintuitiv wollen, um Übereinstimmungen zu kopieren, um zu vermeiden, alle Quellprotokolldaten im RAM zu behalten.

+0

Angesichts der Datenmengen tb behandelt Ich habe die Anzahl der Zeilen zu begrenzen, in einem Durchlauf scannen (Gründe sollte klar sein: durchschnittlich I/O-Last, CPU-Last und resident Satzgröße, dh zu verhindern große Last peaks), also muss ich nicht wirklich streamen. Aber was ist hier wichtiger für mich ist, dass ich nicht wirklich verstehe, was Sie meinen, dass, wenn ich 1M Zeilen überlaufen die nächste Operation wird nicht 1,25M Zeilenzuweisung + kopieren, dass 1M Zeilen? Siehe nächsten Kommentar für die Berechnung. – LetMeSOThat4U

+0

In Python: a = 1; cumul = 0. Dann 'für i im Bereich (10): print 'rows% .2f cumul% .2f'% (a, cumul); cumul + = a; a = a * 1,25'. Ergebnis: Zeilen 1,00 Cumul 0,00 Zeilen 1,25 Cumul 1,00 Zeilen 1,56 Cumul 2,25 Zeilen 1,95 Cumul 3,81 Zeilen 2,44 Cumul 5,77 Zeilen 3,05 Cumul 8,21 Zeilen 3,81 Cumul 11,26 Zeilen 4,77 Cumul 15,07 Zeilen 5,96 Cumul 19,84 Zeilen 7,45 Cumul 25,80.Also, wenn ich mit 1M Zeilen Slice anfange und 8 Millionen Übereinstimmungen habe, müsste ich Slice 10 Mal kopieren und insgesamt 25M Zeilen kopieren. Das ist nicht eine sehr gute Kosten im Vergleich zu Linked List sagen. – LetMeSOThat4U

+0

Ich denke, dass eine Scheibe von Strings ist intern eine Scheibe von Zeigern auf Strings, in dem Sinne, dass die Verlängerung der Scheibe wird nicht wirklich die Zeichenfolgen, nur die Zeiger auf die Strings, also wird jeder Eintrag 4 oder 8 Bytes (abhängig von der Systemarchitektur) sein, also sollte es schnell sein. – siritinga

3

Ich habe versucht, Ihre Frage in ein sehr einfaches Beispiel zu destillieren.

Da es "Hunderttausende von Regex-Übereinstimmungen" geben kann, habe ich eine große Anfangszuweisung von 1 M (1024 * 1024) Einträgen für die matches Schichtkapazität vorgenommen. Ein Slice ist ein Referenztyp. Ein Slice-Header 'struct' hat eine Länge, eine Kapazität und einen Zeiger für insgesamt 24 (3 * 8) Bytes auf einem 64-Bit-Betriebssystem. Die anfängliche Zuweisung für eine Scheibe von 1 M Einträgen beträgt daher nur 24 (24 * 1) MB. Bei mehr als 1 M Einträgen wird ein neuer Slice mit einer Kapazität von 1,25 (1 + 1/4) M Einträgen zugeordnet und die vorhandenen 1 M Slice-Header-Einträge (24 MB) werden dorthin kopiert.

Zusammenfassend können Sie einen Großteil des Overheads vieler append s vermeiden, indem Sie die Slice-Kapazität anfänglich überschreiben. Das größere Speicherproblem sind alle Daten, die für jedes Spiel gespeichert und referenziert werden. Das weitaus größere CPU-Zeitproblem ist die Zeit, die benötigt wird, um die regexp.FindAll's durchzuführen.

package main 

import (
    "bufio" 
    "fmt" 
    "os" 
    "regexp" 
) 

var searches = []*regexp.Regexp{ 
    regexp.MustCompile("configure"), 
    regexp.MustCompile("unknown"), 
    regexp.MustCompile("PATH"), 
} 

var matches = make([][]byte, 0, 1024*1024) 

func main() { 
    logName := "config.log" 
    log, err := os.Open(logName) 
    if err != nil { 
     fmt.Fprintln(os.Stderr, err) 
     os.Exit(1) 
    } 
    defer log.Close() 
    scanner := bufio.NewScanner(log) 
    for scanner.Scan() { 
     line := scanner.Bytes() 
     for _, s := range searches { 
      for _, m := range s.FindAll(line, -1) { 
       matches = append(matches, append([]byte(nil), m...)) 
      } 
     } 
    } 
    if err := scanner.Err(); err != nil { 
     fmt.Fprintln(os.Stderr, err) 
    } 
    // Output matches 
    fmt.Println(len(matches)) 
    for i, m := range matches { 
     fmt.Println(string(m)) 
     if i >= 16 { 
      break 
     } 
    } 
}