逐行处理耗时太长

  • 本文关键字:处理 逐行 go
  • 更新时间 :
  • 英文 :


我正在处理一些非常大的文件,我的简单Go程序需要2分钟的运行时间,而不是等效的C程序需要15秒(https://gist.github.com/g2boojum/5729bf75a41f537b8251af25a816c2fc)。很明显,我错过了一些重要的事情。(这也是我的第一个Go程序,所以我确信代码也很差。(

我正在处理的文件是csv文件,如下所示,唯一的问题是它们的大小是GB。

BOARD;CHANNEL;TIMETAG;ENERGY;ENERGYSHORT;FLAGS
0;0;179096000;316;105;0x0
0;0;682168000;449;146;0x0
0;0;905440000;92;35;0x0

我本可以使用csv模块,但Scanf将字段转换为正确的类型,这似乎更简单。所有代码所做的就是跳过标题行,然后逐行读取文件的其余部分,创建第4个字段的直方图,然后在末尾写入直方图。

import (    
"bufio"    
"fmt"    
"os"     
"log"    
)    

const num_channels int = 4096    

func main () {    
if len(os.Args) != 3 {    
fmt.Printf("Usage: %s infile outfilen", os.Args[0])    
os.Exit(1)    
}    
fin, err := os.Open(os.Args[1])    
if err != nil {    
log.Fatal(err)    
}    
scanner := bufio.NewScanner(fin)               
scanner.Scan() // skip the header line     
fout, err := os.Create(os.Args[2])    
if err != nil {    
log.Fatal(err)    
}    
fmt.Fprintf(fout, "channel,totaln")    
var total[num_channels] uint64    
var tmax int64    
var board, channel, energy, energyshort, flags int    
var timetag int64    
for scanner.Scan() {    
fmt.Sscanf(scanner.Text(), "%d;%d;%d;%d;%d;%x", &board, &channel, &timetag, &energy, &energyshort, &flags)    
total[energy] += 1    
tmax = timetag        
}                                                                                    
tmax_s := float64(tmax)/1e12    
fmt.Println("tmax = ", tmax_s)    
for i, val := range total {         
fmt.Fprintf(fout, "%v,%vn", i, float64(val)/tmax_s)    
}    
}        

帮助?谢谢

[更新,一个解决方案,和一些奇怪的事情]

我简化了一些东西,这样我就可以用更简单的代码更好地了解正在发生的事情。我删除了我用来测试所有内容的csv文件的标题行,还创建了一个更短的csv版本,我可以共享它,以防有人想要测试用例(https://grantgoodyear.org/files/sample60.csv)。

这是一个简化的C代码:

#include <stdio.h>
int main(int argc, char* argv[]) {
FILE* fp = fopen(argv[1], "r");
int board, channel, energy, energyshort, flags;
long timetag;
double tmax = 0;
while ((fscanf(fp, "%d;%d;%ld;%d;%d;%x", &board, &channel, &timetag, &energy, &energyshort, &flags)) != EOF) {
tmax = timetag / 1.0e12;
}
printf("tmax: %fn", tmax);
fclose(fp);
}

它分别在0.16s和15s内处理short和1.5GB文件。

$ time cmaxt sample60.csv 
tmax: 59.999983
real    0m0.160s
user    0m0.152s
sys 0m0.008s
$ time cmaxt long.csv 
tmax: 7200.265511
real    0m14.730s
user    0m14.451s
sys 0m0.255s

相比之下,围棋中有一个几乎相同的程序:

import (
"io"
"fmt"
"os"
)

func main () {
fin, _ := os.Open(os.Args[1])
var tmax float64
var board, channel, energy, energyshort, flags int
var timetag int64
for {
_, err := fmt.Fscanf(fin,"%d;%d;%d;%d;%d;%x", &board, &channel, &timetag, &energy, &energyshort, &flags)
if err == io.EOF {
break
}
tmax = float64(timetag)/1e12
}
fmt.Println("tmax = ", tmax)
}

运行需要惊人的长时间:

$ time gomaxt sample60.csv 
tmax =  59.999983344
real    0m8.044s
user    0m4.677s
sys 0m3.555s
$ time gomaxt long.csv 
tmax =  7200.265510652
real    18m37.472s
user    10m58.221s
sys 8m28.282s

我不知道这里发生了什么,但它比C版本需要50-75倍的时间。特别奇怪的是系统时间这么长。

我更改了go版本,使其更像我原来的帖子,使用了bufio。NewScanner和fmt。Sscanf进行拆分:

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

func main () {
fin, _ := os.Open(os.Args[1])
scanner := bufio.NewScanner(fin)
var tmax float64
var timetag int64
var board, channel, energy, energyshort, flags int
for scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d;%d;%d;%d;%d;%x", &board, &channel, &timetag, &energy, &energyshort, &flags)
tmax = float64(timetag)/1e12
}
fmt.Println("tmax = ", tmax)
}

仅此版本(?!(所需时间是C版本的6倍:

$ time gomaxtorig sample60.csv 
tmax =  59.999983344
real    0m0.895s
user    0m0.905s
sys 0m0.038s
$ time gomaxtorig long.csv 
tmax =  7200.265510652
real    1m53.030s
user    2m1.039s
sys 0m3.021s

现在让我们来替换fmt。带字符串拆分的Sscanf:

import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)

func main () {
fin, _ := os.Open(os.Args[1])
scanner := bufio.NewScanner(fin)
var tmax float64
var timetag int64
for scanner.Scan() {
ss := strings.Split(scanner.Text(), ";")
timetag, _ = strconv.ParseInt(ss[2], 10, 64)
tmax = float64(timetag)/1e12
}
fmt.Println("tmax = ", tmax)
}

正如建议的那样,大部分时间实际上都花在了fmt上。Sscanf。这个版本的时间是C版本的两倍:

$ time ../gammaprocess_go/maxtsplit sample60.csv 
tmax =  59.999983344
real    0m0.226s
user    0m0.243s
sys 0m0.022s
$ time ../gammaprocess_go/maxtsplit long.csv 
tmax =  7200.265510652
real    0m26.434s
user    0m28.834s
sys 0m1.683s

我确实写了一个有点粗糙的版本,强制csv文件每行中其他字段的字符串转换,只是想看看这是否有什么不同,时间与上面的版本基本相同。

因为我在用扫描仪。Text((,有很多字符串被创建然后抛出,有人建议我使用字节而不是字符串。在我看来,csv包就是这么做的,所以我只使用了它:

import (
"encoding/csv"
"fmt"
"os"
"io"
"strconv"
)

func main () {
fin, _ := os.Open(os.Args[1])
r := csv.NewReader(fin)
r.Comma = ';'
var tmax float64
var timetag int64
for {
rec, err := r.Read()
if err == io.EOF {
break
}
timetag, _ = strconv.ParseInt(rec[2], 10, 64)
tmax = float64(timetag)/1e12
}
fmt.Println("tmax = ", tmax)
}

时间只比使用Scanner的字符串拆分版本稍长。文本*((:

$ time gomaxtcsv sample60.csv 
tmax =  59.999983344
real    0m0.281s
user    0m0.300s
sys 0m0.019s
$ time gomaxtcsv long.csv 
tmax =  7200.265510652
real    0m32.736s
user    0m35.619s
sys 0m1.702s

但可能会有更多的开销,因为csv包比我的简单示例做得更多,所以我认为这是不确定的。

无论如何,我对2倍的低效率很满意,所以我不会继续优化它。非常感谢回答这个问题的人。

【另一更新】

或者只是看看https://groups.google.com/g/golang-nuts/c/W08rFBcHKbc/m/oIrdQcBxKa4J,自2014年起。

sscanf占用了大部分时间。Do:

ss := strings.Split(scanner.Text(), ";")
board, _ = strconv.Atoi(ss[0])
channel, _ = strconv.Atoi(ss[1])
timetag, _ = strconv.Atoi(ss[2])
energy, _ = strconv.Atoi(ss[3])
flags, _ = strconv.ParseUint(ss[4], 16, 64)

省略了检查错误。

最新更新