J'essaie de trouver le moyen le plus rapide de lire un gros fichier ligne par ligne et de vérifier si la ligne contient une chaîne. Le fichier sur lequel je teste fait environ 680 Mo
buf := make([]byte, 64*1024) scanner.Buffer(buf, bufio.MaxScanTokenSize)
Après avoir construit le programme et le chronométrer sur ma machine, il s'exécute sur 3 secondes
./speed 3.13s user 1.25s system 122% cpu 3.563 total
Après avoir augmenté le tampon
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
f, err := os.Open("./crackstation-human-only.txt")
scanner := bufio.NewScanner(f)
if err != nil {
panic(err)
}
defer f.Close()
for scanner.Scan() {
if strings.Contains(scanner.Text(), "Iforgotmypassword") {
fmt.Println(scanner.Text())
}
}
}
Il obtient un un peu mieux
./speed 2.47s user 0.25s system 104% cpu 2.609 total
Je sais que cela peut s'améliorer car d'autres outils gèrent pour le faire en moins d'une seconde sans aucune sorte d'indexation. Quel semble être le goulot d'étranglement avec cette approche?
0.33s user 0.14s system 94% cpu 0.501 total
4 Réponses :
Vous pouvez essayer d'utiliser des goroutines pour traiter plusieurs lignes en parallèle:
lines := make(chan string, numWorkers * 2) // give the channel enough room to put lots of things in so the reader isn't blocked
go func(scanner *bufio.Scanner, out <-chan string) {
for scanner.Scan() {
out <- scanner.Text()
}
close(out)
}(scanner, lines)
var wg sync.WaitGroup
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func(in chan<- string) {
defer wg.Done()
for text := range in {
if strings.Contains(text, "Iforgotmypassword") {
fmt.Println(scanner.Text())
}
}
}(lines)
}
wg.Wait()
Je ne sais pas dans quelle mesure cela accélérera vraiment les choses car cela dépend du type de matériel dont vous disposez ; il semble que vous recherchiez une amélioration de la vitesse supérieure à 5x, vous remarquerez peut-être si vous exécutez quelque chose qui peut prendre en charge 8 threads de travail parallèles. N'hésitez pas à utiliser beaucoup d'ouvriers-goroutines. Bonne chance.
Le scanner fonctionne toujours sans parallèle et c'est la partie la plus lente, semble-t-il.
J'ai essayé cela, c'était beaucoup plus lent que l'original avec 8 ouvriers, 85 sec contre 2,2s pour l'original. La surcharge de planification et de copie dans la mémoire tampon était supérieure au traitement effectué. Il y a aussi un bug! Il doit s'agir de fmt.Println (texte) , pas de fmt.Println (scanner.Text ()) , sinon il imprime une ligne différente de celle correspondante.
DERNIÈRE MODIFICATION
Il s'agit d'une solution "ligne par ligne" au problème qui prend un temps insignifiant, elle imprime toute la ligne correspondante.
dat, _ := ioutil.ReadFile("./jumble.txt")
sdat := bytes.Split(dat, []byte{'\n'})
for _, l := range sdat {
if bytes.Equal([]byte("Iforgotmypassword"), l) {
fmt.Println("Yes")
}
}
Cela accélère vraiment beaucoup ./speed 0.35s user 0.49s system 99% cpu 0.845 total , mais ce que j'essaie de comprendre, c'est où est le cou bootle quand on va ligne par ligne
Merci pour la réponse exhaustive. En suivant cette logique, augmenter la mémoire tampon à 1 Go le rendrait également rapide, car tout serait en mémoire, mais cela ne semble pas être le cas. Il tourne toujours plus lentement
En utilisant mon propre fichier de test de 700 Mo avec votre original, le temps était d'un peu plus de 7 secondes
Avec grep, il était de 0,49 seconde
Avec ce programme (qui n'imprime pas la ligne, il dit oui) 0,082 seconde
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
find := []byte(os.Args[1])
dat, err := ioutil.ReadFile("crackstation-human-only.txt")
check(err)
if bytes.Contains(dat, find) {
fmt.Print("yes")
}
}
C'est en effet rapide et fonctionne, mais je ne comprends toujours pas pourquoi la solution ligne par ligne échoue
Une solution ligne par ligne doit vérifier chaque octet une fois pour voir s'il s'agit d'une nouvelle ligne, diviser ces octets en morceaux en fonction de l'emplacement des nouvelles lignes, puis vérifier à nouveau ces morceaux par rapport à la chaîne que vous recherchez. Puisque vous considérerez que n'importe quelle ligne correspond si elle contient la chaîne que vous voulez, c'est bien de n'effectuer qu'une seule itération dans le fichier, en traitant le tout comme une séquence de gros octets.
H. La réponse de Ross est géniale, mais elle lit le fichier entier en mémoire, ce qui peut ne pas être faisable si votre fichier est trop gros. Si vous souhaitez toujours numériser ligne par ligne, peut-être si vous recherchez plusieurs éléments, j'ai trouvé que l'utilisation de scanner.Bytes () au lieu de scanner.Text () améliore légèrement la vitesse sur ma machine, à partir de 2,244 s pour le question originale, à 1.608s. La méthode scanner.Bytes () de bufio n'alloue pas de mémoire supplémentaire, tandis que Text () crée une chaîne à partir de son tampon.
package main
import (
"bufio"
"fmt"
"os"
"bytes"
)
// uses scanner.Bytes to avoid allocation.
func main() {
f, err := os.Open("./crackstation-human-only.txt")
scanner := bufio.NewScanner(f)
if err != nil {
panic(err)
}
defer f.Close()
toFind := []byte("Iforgotmypassword")
for scanner.Scan() {
if bytes.Contains(scanner.Bytes(), toFind) {
fmt.Println(scanner.Text())
}
}
}
scanner.Textnécessite une conversion de chaîne, donc utilisezscanner.Bytesà la place etbytes.Containsau lieu destring.Contains code > pourrait être plus rapide.fmt.Printlnn'est pas non plus le plus rapide et, tel qu'il est écrit, implique d'effectuer la conversion de chaîne une deuxième fois. Mais d'une manière générale, vous devez profiler le code pour trouver où vous perdez de la vitesse.la suppression des conversions de chaînes n'aide pas non plus. si je n'ai qu'un compteur de ligne
counter: = 0 pour scanner.Scan () {counter ++}ça ne va pas plus vite, juste un petit peu. Il semble donc que le col du bootle soit scanner.Scan () mais je ne sais pas comment l'améliorer.Avez-vous essayé de définir un tampon plus grand? Il n'y a pas non plus d'informations de synchronisation dans le code ici, comment exécutez-vous votre programme?
Juste pour confirmer, chronométrez-vous un exécutable construit ou chronométrez-vous
go run?time ./programname qui donne
./programname 2.53s user 0.49s system 99% cpu 3.025 totalJ'ai édité ma réponse pour donner un programme d'exécution court qui correspondra à des lignes entières.