J'ai écrit le processus de lecture du texte complet à partir du vidage Wiktionary en Python, mais je vais le porter sur F # et comparer la vitesse de traitement.
Ceci est une série d'articles.
Le script de cet article est publié dans le référentiel suivant.
Quand j'ai commencé à vider Wiktionary pour la première fois, je pensais utiliser F #, mais le .NET Framework ne pouvait pas gérer bzip2 par défaut, j'ai donc commencé à l'implémenter en Python. Une fois mis en parallèle, le processus s'est terminé en un peu plus d'une minute, j'ai donc estimé que Python était suffisant en termes de vitesse.
Pourtant, je me demandais à quelle vitesse F # serait, alors je vais essayer de compenser la bibliothèque manquante.
L'environnement utilisé pour la mesure est le suivant.
Une bibliothèque externe est requise pour fonctionner avec bzip2 dans le .NET Framework.
Pour commencer, lisez toutes les lignes du vidage et comptez le nombre de lignes. Répertoriez le code Python et la durée correspondants.
Langue | code | Temps requis |
---|---|---|
Python | python/research/countlines.py | 3m34.911s |
F# | fsharp/research/countlines.fsx | 2m40.270s |
commander | bzcat FILE.xml.bz2 | wc -l |
2m32.203s |
F # est la vitesse à l'approche de la commande.
#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)
Lisez le fichier bzip2 séparément pour chaque flux.
Masquez et lisez FileStream
pour éviter la surcharge de passer par les octets. Utilisez le SubStream
créé dans l'article suivant.
Utilisez les données de longueur de flux (streamlen.tsv
) générées dans l'article précédent (https://qiita.com/7shi/items/e8091f6ac72491ad45a6).
Langue | code | Temps requis |
---|---|---|
Python | python/research/countlines-BytesIO.py | 3m37.827s |
F# | fsharp/research/countlines-split.fsx | 5m23.122s |
Apparemment, il y a une surcharge non négligeable au démarrage de la lecture du BZip2Stream, qui est assez lente. J'ai utilisé SubStream
pour réduire encore un peu les frais généraux, mais je ne peux pas du tout rattraper.
#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)
Une fois parallélisé en Python, je lis tous les 10 flux pour réduire la surcharge de la communication inter-processus, mais je prends la même action. Essayez également de lire tous les 100 flux à des fins de comparaison.
Langue | code | Temps requis | Remarques |
---|---|---|---|
F# | fsharp/research/countlines-split-10.fsx | 2m50.913s | Tous les 10 flux |
F# | fsharp/research/countlines-split-100.fsx | 2m40.727s | Tous les 100 flux |
C'est beaucoup plus rapide. Étant donné que tous les 100 flux sont plus rapides, l'étape suivante consiste à fractionner tous les 100 flux.
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)
Divisez par 100 éléments avec Seq.chunkBySize
et additionnez avec ʻArray.sum`.
En Python, il était plus rapide de se convertir en une chaîne et d'utiliser StringIO
. Essayez le même processus avec F #.
Langue | code | Temps requis | Remarques |
---|---|---|---|
Python | python/research/countlines-StringIO.py | 3m18.568s | Par flux |
F# | fsharp/research/countlines-split-string.fsx | 7m50.915s | Par flux |
F# | fsharp/research/countlines-split-string-10.fsx | 3m55.453s | Tous les 10 flux |
F# | fsharp/research/countlines-split-string-100.fsx | 3m23.417s | Tous les 100 flux |
Cette méthode est rejetée car elle n'est pas très rapide en F #.
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)
Extrayez le contenu de la balise XML <text>
et comptez le nombre de lignes.
partie commune
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)
Essayez différentes méthodes. Remplace getPages
par la méthode.
Analyse les balises avec le traitement des chaînes ligne par ligne.
Langue | code | Temps requis | Remarques |
---|---|---|---|
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 | tranche |
F# | fsharp/research/countlines-text-split.fsx | 4m05.507s | Substring |
La lenteur de StartsWith
dans le .NET Framework semble être due à un traitement tel que la normalisation Unicode.
En utilisant Substring
pour faire l'équivalent de StartsWith
, c'est finalement à peu près aussi rapide que 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() }}
Créez et remplacez une fonction qui remplace «StartsWith».
tranche
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
Comparez comment créer une arborescence avec un analyseur XML et comment analyser simplement sans créer d'arborescence.
L'élément racine est requis pour l'analyse XML. Comme nous l'avons déjà vu, F # est lent lors du passage de chaînes, nous combinons donc les flux pour le traitement. Utilisez le ConcatStream
créé dans l'article suivant.
Langue | code | Temps requis | Remarques |
---|---|---|---|
Python | python/research/countlines-text-xml.py | 5m50.826s | ElementTree.fromstring |
F# | fsharp/research/countlines-text-split-doc.fsx | 6m21.588s | XmlDocument |
F # est plus lent que 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() }}
Veuillez vous référer aux articles suivants pour divers analyseurs Python.
F # utilise un XmlReader de type pull.
Langue | code | Temps requis | Remarques |
---|---|---|---|
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(indivis) |
Le .NET Framework XmlDocument
est construit à l'aide de XmlReader
. «XmlReader» est extrêmement plus rapide à utiliser seul. Dans les étapes qui suivent, nous n'utiliserons que la méthode XmlReader
.
La situation devrait être la même pour Python, mais il semble que la création d'un arbre est beaucoup plus efficace et la création d'un arbre est plus rapide.
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() }
| _ -> () }
À partir du code jusqu'à présent, nous allons extraire les parties communes.
Créez une table dont les éléments contiennent des données dans quelle langue (ʻoutput1.tsv). Le nom de la langue est normalisé dans une autre table (ʻoutput2.tsv
).
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
Puisqu'un article est séparé par la ligne == language name ==
, il sera détecté et associé à id. Distinguer parce que === nom de l'élément ===
est utilisé dans les en-têtes inférieurs.
Comme nous l'avons déjà vu, «StartsWith» est lent. Comparez comment rechercher un caractère à la fois depuis le début d'une ligne avec une expression régulière.
L'analyse XML utilise le traitement des chaînes en Python et XmlReader
en F #.
Langue | code | Temps requis | Remarques |
---|---|---|---|
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 | Caractère par personnage |
F# | fsharp/research/checklang.fsx | 3m24.302s | Caractère par personnage |
Python | python/research/checklang-re.py | 5m9.869s | Expressions régulières |
F# | fsharp/research/checklang-re.fsx | 3m46.270s | Expressions régulières |
En F #, il est rapide de rechercher caractère par caractère, mais c'est lourd à mettre en œuvre et pas polyvalent. Les expressions régulières sont presque aussi rapides que «StartsWith». Compte tenu de la polyvalence, il semble sûr d'utiliser des expressions régulières.
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()
Caractère par personnage
for line in text do
if line.Length >= 3 && line.[0] = '=' && line.[1] = '=' && line.[2] <> '=' then
Expressions régulières
for line in text do
let m = r.Match line
if m.Success then
let lang = m.Groups.[1].Value.Trim()
F # utilise le multithreading et Python utilise le multiprocessus pour paralléliser.
Langue | code | Temps requis | Remarques |
---|---|---|---|
Python | python/research/checklang-parallel.py | 1m16.566s | startswith |
F# | fsharp/research/checklang-parallel.fsx | 1m03.941s | Caractère par personnage |
Python | python/research/checklang-parallel-re.py | 1m19.372s | Expressions régulières |
F# | fsharp/research/checklang-parallel-re.fsx | 1m07.009s | Expressions régulières |
C'est une marge étroite. La croissance due à la parallélisation est plus importante en Python.
Caractère par personnage
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() ]}
Expressions régulières
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
Mesuré avec .NET Core et Mono sur WSL1. Comparez avec les résultats .NET Framework sous Windows.
La correspondance avec l'abréviation est la suivante.
code | Framework | Core | Mono | Remarques |
---|---|---|---|---|
fsharp/research/checklang.fsx | 3m24.302s | 3m25.545s | 4m22.330s | |
fsharp/research/checklang-re.fsx | 3m46.270s | 3m42.882s | 4m51.236s | Expressions régulières |
fsharp/research/checklang-parallel.fsx | 1m03.941s | 0m59.014s | 2m39.716s | Parallèle |
fsharp/research/checklang-parallel-re.fsx | 1m07.009s | 1m06.136s | 3m28.074s | Expression régulière parallèle |
.NET Core semble être aussi rapide ou légèrement plus rapide que le .NET Framework.
.NET Core consiste essentiellement à créer un projet, mais c'était gênant car il y avait plusieurs fichiers exécutables, j'ai donc écrit la configuration par la méthode suivante et je l'ai fait.
L'utilisation de la méthode la plus rapide a donné un F # légèrement plus rapide. C'était impressionnant que Python soit plus rapide même s'il était porté avec le même contenu de traitement.
Comme je l'ai essayé dans l'article suivant, il existe une énorme différence dans le traitement basé sur les caractères.
Recommended Posts