Geschwindigkeitsvergleich der Volltextverarbeitung von Wiktionary mit F # und Python

Ich habe den Prozess des Lesens des vollständigen Textes aus dem Wiktionary-Dump in Python geschrieben, aber ich werde ihn auf F # portieren und die Verarbeitungsgeschwindigkeit vergleichen.

Dies ist eine Reihe von Artikeln.

  1. Erkundung effizienter Verarbeitungsmethoden für Wiktionary
  2. Vergleich der Wiktionary-Verarbeitungsgeschwindigkeit zwischen F # und Python ← Dieser Artikel

Das Skript für diesen Artikel befindet sich im folgenden Repository.

Überblick

Als ich anfing, Wiktionary zu sichern, dachte ich, ich würde F # verwenden, aber .NET Framework konnte bzip2 standardmäßig nicht verarbeiten, also begann ich, es in Python zu implementieren. Nach der Parallelisierung war der Vorgang in etwas mehr als einer Minute abgeschlossen, sodass ich der Meinung war, dass Python in Bezug auf die Geschwindigkeit ausreichend war.

Trotzdem habe ich mich gefragt, wie schnell F # sein würde, also werde ich versuchen, die fehlende Bibliothek auszugleichen.

Die für die Messung verwendete Umgebung ist wie folgt.

Anzahl der Zeilen

Für die Arbeit mit bzip2 in .NET Framework ist eine externe Bibliothek erforderlich.

Lesen Sie zunächst alle Zeilen im Speicherauszug und zählen Sie die Anzahl der Zeilen. Listen Sie den entsprechenden Python-Code und die Dauer auf.

Sprache Code Benötigte Zeit
Python python/research/countlines.py 3m34.911s
F# fsharp/research/countlines.fsx 2m40.270s
Befehl bzcat FILE.xml.bz2 | wc -l 2m32.203s

F # ist die Geschwindigkeit, die sich dem Befehl nähert.

#r "AR.Compression.BZip2.dll"
open System
open System.IO
open System.IO.Compression

let target = "enwiktionary-20200501-pages-articles-multistream.xml.bz2"
let mutable lines = 0
do
    use fs = new FileStream(target, FileMode.Open)
    use bs = new BZip2Stream(fs, CompressionMode.Decompress)
    use sr = new StreamReader(bs)
    while not (isNull (sr.ReadLine())) do
        lines <- lines + 1
Console.WriteLine("lines: {0:#,0}", lines)

Stream aufgeteilt

Lesen Sie die bzip2-Datei für jeden Stream separat.

Maskieren und lesen Sie "FileStream", um den Aufwand beim Durchlaufen der Bytes zu vermeiden. Verwenden Sie den im folgenden Artikel erstellten "SubStream".

Verwenden Sie die im vorherigen Artikel (https://qiita.com/7shi/items/e8091f6ac72491ad45a6) generierten Daten zur Stream-Länge (streamlen.tsv).

Sprache Code Benötigte Zeit
Python python/research/countlines-BytesIO.py 3m37.827s
F# fsharp/research/countlines-split.fsx 5m23.122s

Anscheinend ist der Start des BZip2Stream-Lesevorgangs mit einem nicht zu vernachlässigenden Aufwand verbunden, der recht langsam ist. Ich habe "SubStream" verwendet, um den Overhead ein wenig zu reduzieren, aber ich kann überhaupt nicht aufholen.

#r "AR.Compression.BZip2.dll"
#load "StreamUtils.fsx"
open System
open System.IO
open System.IO.Compression
open StreamUtils

let target, slen =
    use sr = new StreamReader("streamlen.tsv")
    sr.ReadLine(),
    [|  while not <| sr.EndOfStream do
        yield sr.ReadLine() |> Convert.ToInt32 |]

let mutable lines = 0
do
    use fs = new FileStream(target, FileMode.Open)
    for length in slen do
        use ss = new SubStream(fs, length)
        use bs = new BZip2Stream(ss, CompressionMode.Decompress)
        use sr = new StreamReader(bs)
        while not (isNull (sr.ReadLine())) do
            lines <- lines + 1
Console.WriteLine("lines: {0:#,0}", lines)

Vermeiden Sie Overhead

Bei der Parallelisierung in Python lese ich alle 10 Streams, um den Overhead der Kommunikation zwischen Prozessen zu verringern, aber ich ergreife die gleiche Aktion. Versuchen Sie auch, alle 100 Streams zum Vergleich zu lesen.

Sprache Code Benötigte Zeit Bemerkungen
F# fsharp/research/countlines-split-10.fsx 2m50.913s Alle 10 Streams
F# fsharp/research/countlines-split-100.fsx 2m40.727s Alle 100 Streams

Es ist viel schneller. Da alle 100 Streams schneller sind, wird im nächsten Schritt alle 100 Streams aufgeteilt.

let mutable lines = 0
do
    use fs = new FileStream(target, FileMode.Open)
    for lengths in Seq.chunkBySize 100 slen do
        use ss = new SubStream(fs, Array.sum lengths)
        use bs = new BZip2Stream(ss, CompressionMode.Decompress)
        use sr = new StreamReader(bs)
        while not (isNull (sr.ReadLine())) do
            lines <- lines + 1
Console.WriteLine("lines: {0:#,0}", lines)

Teilen Sie mit Seq.chunkBySize durch 100 Elemente und summieren Sie mit Array.sum.

String-Konvertierung

In Python war es schneller, in einen String zu konvertieren und "StringIO" zu verwenden. Versuchen Sie den gleichen Vorgang mit F #.

Sprache Code Benötigte Zeit Bemerkungen
Python python/research/countlines-StringIO.py 3m18.568s Pro Stream
F# fsharp/research/countlines-split-string.fsx 7m50.915s Pro Stream
F# fsharp/research/countlines-split-string-10.fsx 3m55.453s Alle 10 Streams
F# fsharp/research/countlines-split-string-100.fsx 3m23.417s Alle 100 Streams

Diese Methode wird abgelehnt, da sie in F # nicht sehr schnell ist.

let mutable lines = 0
do
    use fs = new FileStream(target, FileMode.Open)
    for length in slen do
        let text =
            use ss = new SubStream(fs, length)
            use bs = new BZip2Stream(ss, CompressionMode.Decompress)
            use ms = new MemoryStream()
            bs.CopyTo(ms)
            Encoding.UTF8.GetString(ms.ToArray())
        use sr = new StringReader(text)
        while not (isNull (sr.ReadLine())) do
            lines <- lines + 1
Console.WriteLine("lines: {0:#,0}", lines)

Textextraktion

Extrahieren Sie den Inhalt des XML-Tags "" und zählen Sie die Anzahl der Zeilen.

allgemeiner Teil


let mutable lines = 0
do
    use fs = new FileStream(target, FileMode.Open)
    fs.Seek(int64 slen.[0], SeekOrigin.Begin) |> ignore
    for lengths in Seq.chunkBySize 100 slen.[1 .. slen.Length - 2] do
        for _, text in getPages(fs, Array.sum lengths) do
            for _ in text do
                lines <- lines + 1
Console.WriteLine("lines: {0:#,0}", lines)

Probieren Sie verschiedene Methoden aus. Ersetzt getPages durch die Methode.

String-Verarbeitung

Analysiert Tags mit Zeichenfolgenverarbeitung Zeile für Zeile.

Sprache Code Benötigte Zeit Bemerkungen
Python python/research/countlines-text.py 4m06.555s startswith
F# fsharp/research/countlines-text-split-StartsWith.fsx 4m42.877s StartsWith
F# fsharp/research/countlines-text-split-slice.fsx 4m14.069s Scheibe
F# fsharp/research/countlines-text-split.fsx 4m05.507s Substring

Die Langsamkeit von "StartsWith" in .NET Framework scheint auf die Verarbeitung wie die Unicode-Normalisierung zurückzuführen zu sein.

Wenn Sie "Substring" verwenden, um das Äquivalent zu "StartsWith" zu erreichen, ist es endlich ungefähr so schnell wie Python.

StartsWith


let getPages(stream, length) = seq {
    use ss = new SubStream(stream, length)
    use bs = new BZip2Stream(ss, CompressionMode.Decompress)
    use sr = new StreamReader(bs)
    let mutable ns, id = 0, 0
    while sr.Peek() <> -1 do
        let mutable line = sr.ReadLine().TrimStart()
        if line.StartsWith "<ns>" then
            ns <- Convert.ToInt32 line.[4 .. line.IndexOf('<', 4) - 1]
            id <- 0
        elif id = 0 && line.StartsWith "<id>" then
            id <- Convert.ToInt32 line.[4 .. line.IndexOf('<', 4) - 1]
        elif line.StartsWith "<text " then
            let p = line.IndexOf '>'
            if line.[p - 1] = '/' then () else
            if ns <> 0 then
                while not <| line.EndsWith "</text>" do
                line <- sr.ReadLine()
            else
                line <- line.[p + 1 ..]
                yield id, seq {
                    while not <| isNull line do
                    if line.EndsWith "</text>" then
                        if line.Length > 7 then yield line.[.. line.Length - 8]
                        line <- null
                    else
                        yield line
                        line <- sr.ReadLine() }}

Erstellen und ersetzen Sie eine Funktion, die "StartsWith" ersetzt.

Scheibe


let inline startsWith (target:string) (value:string) =
    target.Length >= value.Length && target.[.. value.Length - 1] = value

Substring


let inline startsWith (target:string) (value:string) =
    target.Length >= value.Length && target.Substring(0, value.Length) = value

XML-Parser

Vergleichen Sie, wie Sie einen Baum mit einem XML-Parser erstellen und wie Sie nur analysieren, ohne einen Baum zu erstellen.

Das Root-Element wird zum Parsen von XML benötigt. Wie wir bereits gesehen haben, ist F # beim Durchlaufen von Strings langsam, daher kombinieren wir Streams für die Verarbeitung. Verwenden Sie den im folgenden Artikel erstellten "ConcatStream".

Da ist ein Baum

Sprache Code Benötigte Zeit Bemerkungen
Python python/research/countlines-text-xml.py 5m50.826s ElementTree.fromstring
F# fsharp/research/countlines-text-split-doc.fsx 6m21.588s XmlDocument

F # ist langsamer als Python.

let getPages(stream, length) = seq {
    use ss = new SubStream(stream, length)
    use cs = new ConcatStream([
        new MemoryStream("<pages>"B)
        new BZip2Stream(ss, CompressionMode.Decompress)
        new MemoryStream("</pages>"B) ])
    use xr = XmlReader.Create(cs)
    let doc = XmlDocument()
    doc.Load(xr)
    for page in doc.ChildNodes.[0].ChildNodes do
        let ns = Convert.ToInt32(page.SelectSingleNode("ns").InnerText)
        if ns = 0 then
            let id = Convert.ToInt32(page.SelectSingleNode("id").InnerText)
            let text = page.SelectSingleNode("revision/text").InnerText
            use sr = new StringReader(text)
            yield id, seq {
                while sr.Peek() <> -1 do
                    yield sr.ReadLine() }}

Kein Baum

In den folgenden Artikeln finden Sie Informationen zu verschiedenen Python-Parsern.

F # verwendet einen XmlReader vom Pull-Typ.

Sprache Code Benötigte Zeit Bemerkungen
Python python/research/countlines-text-xmlparser.py 6m46.163s XMLParser
Python python/research/countlines-text-xmlpull.py 6m04.553s XMLPullParser
Python python/research/countlines-text-xmliter.py 6m29.298s ElementTree.iterparse
F# fsharp/research/countlines-text-split-reader.fsx 3m17.916s XmlReader
F# fsharp/research/countlines-text-reader.fsx 3m16.122s XmlReader(ungeteilt)

Das .NET Framework "XmlDocument" wird mit "XmlReader" erstellt. XmlReader ist überwältigend schneller alleine zu benutzen. In den folgenden Schritten verwenden wir nur die XmlReader-Methode.

Die Situation sollte für Python dieselbe sein, aber es scheint, dass das Erstellen eines Baums viel effizienter und das Erstellen eines Baums schneller ist.

let getPages(stream, length) = seq {
    use ss = new SubStream(stream, length)
    use cs = new ConcatStream([
        new MemoryStream("<pages>"B)
        new BZip2Stream(ss, CompressionMode.Decompress)
        new MemoryStream("</pages>"B) ])
    use xr = XmlReader.Create(cs)
    let mutable ns, id = 0, 0
    while xr.Read() do
        if xr.NodeType = XmlNodeType.Element then
            match xr.Name with
            | "ns" ->
                if xr.Read() then ns <- Convert.ToInt32 xr.Value
                id <- 0
            | "id" ->
                if id = 0 && xr.Read() then id <- Convert.ToInt32 xr.Value
            | "text" ->
                if ns = 0 && not xr.IsEmptyElement && xr.Read() then
                    yield id, seq {
                        use sr = new StringReader(xr.Value)
                        while sr.Peek() <> -1 do
                            yield sr.ReadLine() }
            | _ -> () }

Sprachtabelle

Aus dem bisherigen Code werden wir die gemeinsamen Teile herausdrücken.

Erstellen Sie eine Tabelle, deren Elemente Daten in welcher Sprache enthalten (output1.tsv). Der Sprachname wird auf eine andere Tabelle (output2.tsv) normalisiert.

do
    use sw = new StreamWriter("output1.tsv")
    sw.NewLine <- "\n"
    for id, lid in results do
        fprintfn sw "%d\t%d" id lid
do
    use sw = new StreamWriter("output2.tsv")
    sw.NewLine <- "\n"
    for kv in langs do
        fprintfn sw "%d\t%s" kv.Value kv.Key

Da ein Artikel durch die Zeile "== Sprachname ==" getrennt ist, wird er erkannt und mit der ID verknüpft. Unterscheiden Sie, weil in den unteren Überschriften === item name === verwendet wird.

Wie wir bereits gesehen haben, ist "StartsWith" langsam. Vergleichen Sie, wie Sie jeweils ein Zeichen vom Anfang einer Zeile mit einem regulären Ausdruck nachschlagen.

Bei der XML-Analyse wird die Zeichenfolgenverarbeitung in Python und "XmlReader" in F # verwendet.

Sprache Code Benötigte Zeit Bemerkungen
Python python/research/checklang.py 4m26.421s startswith
F# fsharp/research/checklang-StartsWith.fsx 3m43.965s StartsWith
Python python/research/checklang-ch.py 4m30.566s Zeichen für Zeichen
F# fsharp/research/checklang.fsx 3m24.302s Zeichen für Zeichen
Python python/research/checklang-re.py 5m9.869s Reguläre Ausdrücke
F# fsharp/research/checklang-re.fsx 3m46.270s Reguläre Ausdrücke

In F # ist es schnell, Zeichen für Zeichen nachzuschlagen, aber die Implementierung ist umständlich und nicht vielseitig. Reguläre Ausdrücke sind fast so schnell wie "StartsWith". In Anbetracht der Vielseitigkeit scheint es sicher, reguläre Ausdrücke zu verwenden.

StartsWith


            for line in text do
                if line.StartsWith "==" && not <| line.StartsWith "===" then
                    let lang = line.[2..].Trim()
                    let mutable e = lang.Length - 1
                    while e > 0 && lang.[e] = '=' do e <- e - 1
                    let lang = lang.[..e].Trim()

Zeichen für Zeichen


            for line in text do
                if line.Length >= 3 && line.[0] = '=' && line.[1] = '=' && line.[2] <> '=' then

Reguläre Ausdrücke


            for line in text do
                let m = r.Match line
                if m.Success then
                    let lang = m.Groups.[1].Value.Trim()

Parallelisierung

F # verwendet Multithreading und Python verwendet Multiprozess zur Parallelisierung.

Sprache Code Benötigte Zeit Bemerkungen
Python python/research/checklang-parallel.py 1m16.566s startswith
F# fsharp/research/checklang-parallel.fsx 1m03.941s Zeichen für Zeichen
Python python/research/checklang-parallel-re.py 1m19.372s Reguläre Ausdrücke
F# fsharp/research/checklang-parallel-re.fsx 1m07.009s Reguläre Ausdrücke

Es ist ein enger Spielraum. Die Wachstumsbreite aufgrund der Parallelisierung ist in Python größer.

Zeichen für Zeichen


let getlangs(pos, length) = async { return [
    use fs = new FileStream(target, FileMode.Open, FileAccess.Read)
    fs.Seek(pos, SeekOrigin.Begin) |> ignore
    for id, text in MediaWikiParse.getPages(fs, length) do
        for line in text do
            if line.Length >= 3 && line.[0] = '=' && line.[1] = '=' && line.[2] <> '=' then
                let lang = line.[2..].Trim()
                let mutable e = lang.Length - 1
                while e > 0 && lang.[e] = '=' do e <- e - 1
                yield id, lang.[..e].Trim() ]}

Reguläre Ausdrücke


let getlangs(pos, length) = async { return [
    let r = Regex "^==([^=].*)=="
    use fs = new FileStream(target, FileMode.Open, FileAccess.Read)
    fs.Seek(pos, SeekOrigin.Begin) |> ignore
    for id, text in MediaWikiParse.getPages(fs, length) do
        for line in text do
            let m = r.Match line
            if m.Success then
                yield id, m.Groups.[1].Value.Trim() ]}

let results =
    sposlen.[1 .. sposlen.Length - 2]
    |> Seq.chunkBySize 100
    |> Seq.map Array.unzip
    |> Seq.map (fun (ps, ls) -> Array.min ps, Array.sum ls)
    |> Seq.map getlangs
    |> Async.Parallel
    |> Async.RunSynchronously
    |> List.concat
let langs = Dictionary<string, int>()
do
    use sw = new StreamWriter("output1.tsv")
    sw.NewLine <- "\n"
    for id, lang in results do
        let lid =
            if langs.ContainsKey lang then langs.[lang] else
            let lid = langs.Count + 1
            langs.[lang] <- lid
            lid
        fprintfn sw "%d\t%d" id lid
do
    use sw = new StreamWriter("output2.tsv")
    sw.NewLine <- "\n"
    for kv in langs do
        fprintfn sw "%d\t%s" kv.Value kv.Key

.NET Core und Mono

Gemessen mit .NET Core und Mono auf WSL1. Vergleichen Sie mit .NET Framework-Ergebnissen unter Windows.

Die Entsprechung mit der Abkürzung ist wie folgt.

Code Framework Core Mono Bemerkungen
fsharp/research/checklang.fsx 3m24.302s 3m25.545s 4m22.330s
fsharp/research/checklang-re.fsx 3m46.270s 3m42.882s 4m51.236s Reguläre Ausdrücke
fsharp/research/checklang-parallel.fsx 1m03.941s 0m59.014s 2m39.716s Parallel
fsharp/research/checklang-parallel-re.fsx 1m07.009s 1m06.136s 3m28.074s Paralleler, regulärer Ausdruck

.NET Core scheint so schnell oder etwas schneller als .NET Framework zu sein.

.NET Core dient im Grunde genommen zum Erstellen eines Projekts, war jedoch problematisch, da mehrere ausführbare Dateien vorhanden waren. Daher habe ich die Konfiguration mit der folgenden Methode geschrieben und damit fertig.

Impressionen

Die Verwendung der schnellsten Methode führte zu einem etwas schnelleren F #. Es war beeindruckend, dass Python schneller war, selbst wenn es mit demselben Verarbeitungsinhalt portiert wurde.

Wie ich im folgenden Artikel versucht habe, gibt es einen überwältigenden Unterschied in der zeichenbasierten Verarbeitung.

Recommended Posts

Geschwindigkeitsvergleich der Volltextverarbeitung von Wiktionary mit F # und Python
Asynchrone Verarbeitung von Python ~ Asynchron vollständig verstehen und warten ~
Vergleich von CoffeeScript mit JavaScript-, Python- und Ruby-Grammatik
Ich habe versucht, die Verarbeitungsgeschwindigkeit mit dplyr von R und pandas von Python zu vergleichen
Geschwindigkeitsvergleich der Python-XML-Perspektive
Koexistenz von Python2 und 3 mit CircleCI (1.0)
Ich habe die Geschwindigkeit von Hash mit Topaz, Ruby und Python verglichen
Grundlagen der binärisierten Bildverarbeitung durch Python
Zeichnen mit Matrix-Reinventor von Python Image Processing-
Vollständiges Verständnis von Python-Threading und Multiprocessing
Vergleich der Matrixtranspositionsgeschwindigkeit durch Python
Geschwindigkeitsvergleich von murmurhash3, md5 und sha1
Ich habe die numerische Berechnung von Python durch Rust ersetzt und die Geschwindigkeit verglichen
Ich habe 0 Jahre Programmiererfahrung und fordere die Datenverarbeitung mit Python heraus
Rehabilitation von Python- und NLP-Kenntnissen ab "100 Language Processing Knock 2015" (Kapitel 1)
Ich habe die Geschwindigkeit der Listeneinschlussnotation für und während mit Python2.7 gemessen.
Leistungsvergleich des Gesichtsdetektors mit Python + OpenCV
[Python3] Geschwindigkeitsvergleich usw. über den Entzug von numpy.ndarray
Vergleichen Sie die Geschwindigkeit von Python Append und Map
TRIE-Baumimplementierung mit Python und LOUDS
R- und Python-Schreibvergleich (euklidische Methode der gegenseitigen Teilung)
Fortsetzung der Multi-Plattform-Entwicklung mit Electron und Python
Beispiel für das Lesen und Schreiben von CSV mit Python
Vergleich von Python und Ruby (Environment / Grammar / Literal Edition)
Lassen Sie uns mit Python Receive spielen und den Text des Eingabeformulars speichern / anzeigen
Rehabilitation von Python- und NLP-Kenntnissen ab "Knock 100 Language Processing 2015" (Kapitel 2, zweite Hälfte)
Verwendung für Python-Stapel und -Warteschlangen (Geschwindigkeitsvergleich jeder Datenstruktur)
Rehabilitation von Python- und NLP-Kenntnissen ab "100 Language Processing Knock 2015" (Kapitel 2, erste Hälfte)
Verarbeitung von CSV-Daten in voller und halber Breite in Python
Hinweise zur HDR- und RAW-Bildverarbeitung mit Python
Python netCDF4 Lesegeschwindigkeit und Verschachtelung von for-Anweisungen
Experiment zum Vergleich der Schreibgeschwindigkeit von Dateien zwischen Python 2.7.9 und Pypy 2.5.0
Laden Sie mp4 einfach teilweise mit Python und youtube-dl herunter!
[Kapitel 5] Einführung in Python mit 100 Klopfen Sprachverarbeitung
Visualisieren Sie den Bereich der internen und externen Einfügungen mit Python
Textverarbeitung mit Python
Ein schneller Vergleich der Testbibliotheken von Python und node.js.
Fordern Sie die Hauptkomponentenanalyse von Textdaten mit Python heraus
[Kapitel 3] Einführung in Python mit 100 Klopfen Sprachverarbeitung
Bildverarbeitung mit Python
[Kapitel 2] Einführung in Python mit 100 Klopfen Sprachverarbeitung
Vergleichstabelle häufig verwendeter Prozesse von Python und Clojure
Bildverarbeitung mit Python (ich habe versucht, es in 0 und 1 Mosaikkunst zu binarisieren)
Zusammenfassung der Datumsverarbeitung in Python (Datum / Uhrzeit und Datum)
Verwenden Sie Python, das von Pyenv mit Sublime REPL von Sublime Text 3 installiert wurde
Verschiedene Verarbeitung von Python
Versionsverwaltung von Node, Ruby und Python mit anyenv
[Kapitel 4] Einführung in Python mit 100 Klopfen Sprachverarbeitung
Datenbanksuche (Überprüfung der Verarbeitungsgeschwindigkeit mit oder ohne Index)
Führen Sie mit Python und Matplotlib eine Isostromanalyse offener Wasserkanäle durch
[Python] So legen Sie Variablennamen dynamisch fest und vergleichen die Geschwindigkeit
Befreien Sie sich mit Python und regulären Ausdrücken von schmutzigen Daten
Erkennen Sie mit Python Objekte einer bestimmten Farbe und Größe
Vergleich der Verwendung von Funktionen höherer Ordnung in Python 2 und 3
[Lass uns mit Python spielen] Bildverarbeitung zu Monochrom und Punkten
BASIC und C sowie Assembler-Geschwindigkeitsvergleich und -optimierung spielen mit IchigoJam