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.
Das Skript für diesen Artikel befindet sich im folgenden Repository.
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.
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)
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)
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
.
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)
Extrahieren Sie den Inhalt des XML-Tags "
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.
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
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 Stream
s für die Verarbeitung. Verwenden Sie den im folgenden Artikel erstellten "ConcatStream".
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() }}
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() }
| _ -> () }
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()
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
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.
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.